A Firebase Cloud Storage Tutorial
The two preceding chapters have covered the theoretical aspects of Firebase Cloud Storage and the rules that need to be implemented to protect user data. In this chapter, this theory will be put into practice through the creation of an Android app that makes use of cloud storage to store and retrieve video files.
About the Firebase Cloud Storage Example
The app project created in this chapter will make use of the Android video capture intent to allow the user to record a video using the device camera. Once recorded, the user will be given the option to upload the video file to the Firebase cloud and, once uploaded, subsequently download and play the video. Firebase Authentication will be used to keep the videos belonging to different users separate from one another. Storage security rules will also be applied so that only the owner of a video is able to download and view it.
Creating the Project
Launch Android Studio and select the Start a new Android Studio project quick start option from the welcome screen.
Within the new project dialog, enter CloudStorage into the Application name field and your domain as the Company Domain setting before clicking on the Next button.
On the form factors screen, enable the Phone and Tablet option and set the minimum SDK to API 16: Android 4.1 (Jellybean). Continue working through the screens, requesting the creation of an Empty Activity named CloudStorageActivity with a corresponding layout named activity_cloud_storage.
Adding Authentication to the Project
As with most projects where user data is to be protected, Firebase Authentication will be used as the foundation of the security implemented in this chapter. For the purposes of this tutorial, only email/password authentication will be used.
Begin by opening the Firebase console in a browser window and selecting the Firebase Examples project. Navigate to the Authentication screen, select the Sign-In Method tab and enable the Email/Password provider if it is currently disabled.
Return to the project in Android Studio and select the Tools -> Firebase menu option. When the Firebase assistant panel appears, locate and select the Authentication section and click on the Email and password authentication link.
In the resulting panel, click on the Connect to Firebase button to display the Firebase connection dialog.
Choose the option to store the app in an existing Firebase project and select the Firebase Examples project before clicking on the Connect to Firebase button.
With the project’s Firebase connection established, refer to the Firebase assistant panel once again, this time clicking on the Add Firebase Authentication to your app button. A dialog will appear outlining the changes that will be made to the project build configuration to enable Firebase Authentication. Click on the Accept Changes button to implement the changes to the project configuration.
Within the Project tool window, locate and double-click on the build.gradle (Module: app) file so that it loads into the editor and modify the dependencies section to include the firebase-ui and firebase-ui-auth libraries (note that more recent versions of these libraries may have been released since this book was published):
dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support.constraint:constraint-layout:1.0.2' compile 'com.google.firebase:firebase-auth:11.0.1' compile 'com.firebaseui:firebase-ui:2.0.1' compile 'com.firebaseui:firebase-ui-auth:2.0.1' testCompile 'junit:junit:4.12' }
In addition to the FirebaseUI dependencies, the build system will also need to download some additional Twitter library dependencies for the Firebase UI library. These libraries are available via Twitter’s fabric.io project and can be integrated into the Gradle build process for the project by adding a repository entry to the module level build.gradle file as follows:
. . . compile 'com.firebaseui:firebase-ui-auth:1.0.1' compile 'com.google.firebase:firebase-auth:10.0.1' testCompile 'junit:junit:4.12' } repositories { maven { url 'https://maven.fabric.io/public' } } . .
Once the build file changes have been made, click on the Sync Now button located in the yellow bar above the editor window to commit the changes.
Adding the Signed In Activity
The CloudStorageActivity class will be used to authenticate the user. Once the user has signed in to the app, a second activity will be launched within which the user will record, store and playback video.
Add this second activity to the project now by locating the app -> java -> <yourdomain>.cloudstorage entry in the Project tool window and right-clicking on it. When the menu appears, select the New -> Activity -> Empty Activity menu option. In the New Android Activity dialog, name the activity SignedInActivity.
Before clicking on the Finish button, make sure that the Generate Layout File option is enabled and the Launcher Activity option disabled.
Designing the SignedInActivity User Interface
The user interface for the second activity is going to consist of three Buttons, a ProgressBar and a VideoView. Locate the activity_signed_in.xml file, load it into the layout editor and turn off Autoconnect mode using the button located in the toolbar.
Drag and drop three Button widgets from the palette and position them so that they are located along the bottom of the layout as shown in Figure 35‑1. Change the text properties on the three buttons so that they read “Record”, “Upload” and “Download” and configure the onClick property to call methods named record, upload and download respectively:
Figure 35‑1
A constraint layout chain will be used to ensure that the buttons are distributed evenly across the width of the screen, regardless of screen size and device orientation. Add the chain by selecting all three button widgets, right-clicking on the left-most widget and selecting the Center Horizontally option from the resulting menu. With the widgets chained, attach the bottom anchor of each button to the bottom of the parent layout by clicking on the anchor and dragging to the bottom edge of the layout. On completion of these steps, the button layout should match that shown in Figure 35‑2:
Figure 35‑2
The next component to add is the ProgressBar view which will be positioned directly above the row of buttons. From the palette, locate and select the ProgressBar (Horizontal) widget and drag it so that it is positioned above the buttons. Resize the bar so that it extends to cover most of the width of the layout. Using the anchor points, anchor the left and right sides of the bar to the corresponding sides of the parent layout. Using the bottom anchor, constrain the bottom of the bar to the top of the center button. With the ProgressBar widget selected, use the Properties tool window to change the ID to pbar. Also change the layout_width property to 0dp. This switches the width to match constraints mode, causing the view to resize based on the applied constraints.
The progress bar will be used to display the progress of the file upload as a percentage. Before proceeding, therefore, change the max property to 100.
On completion of these steps, the layout should resemble Figure 35‑3:
Figure 35‑3
From the Images category of the palette, drag and drop a VideoView object onto the layout canvas and position and resize the view to match the layout in Figure 35‑4:
Figure 35‑4
With the VideoView selected use the anchor points to constrain the top, left and right sides of the view to the corresponding sides of the parent layout, then connect the bottom anchor point to the top of the ProgressBar widget.
Referring to the Properties tool window, change both the layout_height and layout_width properties to 0dp so that the view is sized to match the constraints set on it. As the final step in the design of the user interface layout, change the ID property of the VideoView to videoView.
Performing the Authentication
With the user interface designed, code can now be added to the project to perform the user authentication. Edit the CloudStorageActivity.java file and modify it as follows to initiate the authentication process:
package com.ebookfrenzy.cloudstorage; import android.content.Intent; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import com.firebase.ui.auth.AuthUI; import com.google.firebase.auth.FirebaseAuth; import com.firebase.ui.auth.ErrorCodes; import com.firebase.ui.auth.IdpResponse; import com.firebase.ui.auth.ResultCodes; import java.util.ArrayList; import java.util.List; public class CloudStorageActivity extends AppCompatActivity { FirebaseAuth auth; private static final int REQUEST_CODE = 101; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_signed_in); auth = FirebaseAuth.getInstance(); if (auth.getCurrentUser() != null) { startActivity(new Intent(this, SignedInActivity.class)); finish(); } else { authenticateUser(); } } . . }
The code added above will call a method named authenticateUser() if the current user is not already signed in. The next step is to add this method:
private void authenticateUser() { List<AuthUI.IdpConfig> providers = new ArrayList<>(); providers.add( new AuthUI.IdpConfig.Builder(AuthUI.EMAIL_PROVIDER).build()); startActivityForResult( AuthUI.getInstance().createSignInIntentBuilder() .setAvailableProviders(providers) .setIsSmartLockEnabled(false) .build(), REQUEST_CODE); }
When the authentication activity is completed, the onActivityResult() method will be called. Implement this method now to launch the SignedInActivity on a successful authentication:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); IdpResponse response = IdpResponse.fromResultIntent(data); if (requestCode == REQUEST_CODE) { if (resultCode == ResultCodes.OK) { startActivity(new Intent(this, SignedInActivity.class)); return; } } else { if (response == null) { // User cancelled Sign-in return; } if (response.getErrorCode() == ErrorCodes.NO_NETWORK) { // Device has no network connection return; } if (response.getErrorCode() == ErrorCodes.UNKNOWN_ERROR) { // Unknown error occurred return; } } }
After making the code changes, compile and run the app, sign in using either a new or existing Firebase user account and verify that the SignedInActivity appears.
Recording Video
Video recording will begin when the user taps the Record button in the SignedInActivity screen. This button is configured to call a method named record() which now needs to be implemented within the SignedInActivity.java file. Video recording will be performed by making use of the built-in Android video capture intent. Before this code is added, some libraries need to be imported and a constant added to act as the request code for the video capture intent and a variable in which to store the Uri of the locally saved video file. Edit the SignedInActivity.java file and modify it as follows:
package com.ebookfrenzy.cloudstorage; import android.content.Intent; import android.provider.MediaStore; import android.net.Uri; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Toast; import android.widget.ProgressBar; public class CloudStorageActivity extends AppCompatActivity { private Uri videoUri; private static final int REQUEST_CODE = 101; . . }
Next, add the code for the record() method:
public void record(View view) { Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); startActivityForResult(intent, REQUEST_CODE); }
When the video capture intent returns, a method named onActivityResult() will be called and passed a request code and an Intent object containing the Uri of the file containing the video footage. This method now needs to be added to the class:
protected void onActivityResult(int requestCode, int resultCode, Intent data) { videoUri = data.getData(); if (requestCode == REQUEST_CODE) { if (resultCode == RESULT_OK) { Toast.makeText(this, "Video saved to:\n" + videoUri, Toast.LENGTH_LONG).show(); } else if (resultCode == RESULT_CANCELED) { Toast.makeText(this, "Video recording cancelled.", Toast.LENGTH_LONG).show(); } else { Toast.makeText(this, "Failed to record video", Toast.LENGTH_LONG).show(); } } }
Compile and run the app and make sure that the Record button successfully launches the video capture intent. Within the video capture screen, tap the record button and record some video. After recording a few seconds of video, tap the stop button followed by the confirmation button (usually a blue button displaying a white check mark). The intent should return to the SignedInActivity screen where a Toast message will appear displaying the path to the video file on the local device storage.
Now that video is being captured it is now time to add code to upload the file to cloud storage. Before adding this code, however, Firebase Cloud Storage support first needs to be added to the project.
Adding Firebase Cloud Storage Support
Within Android Studio, select the Tools -> Firebase menu option and, in the Firebase assistant panel, unfold the Storage option and click on the Upload and download a file with Firebase Storage link. On the resulting page, click on the Add Firebase Storage to your app button. In the confirmation dialog, review and then accept the changes:
Figure 35‑5
Uploading the Video File to the Cloud
The video file is to be uploaded when the user clicks the Upload button within the SignedInActivity screen. Before the upload can be performed, however, a storage reference needs to be created. In creating the storage reference, the current user’s ID will be included as a component in the path as a way to separate user files. The storage reference path used in this app will read as follows:
/videos/<uid>/userIntro.3gp
Modify the SignedInActivity.java file to add some imports and variables, then add code to the onCreate() method to initialize a storage reference object:
import android.support.annotation.NonNull; import android.widget.VideoView; import android.provider.MediaStore; . . import com.google.android.gms.tasks.OnFailureListener; import com.google.android.gms.tasks.OnSuccessListener; import com.google.firebase.storage.FirebaseStorage; import com.google.firebase.storage.OnProgressListener; import com.google.firebase.storage.StorageReference; import com.google.firebase.storage.FileDownloadTask; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.storage.UploadTask; import java.io.File; . . public class SignedInActivity extends AppCompatActivity { private Uri videoUri; private static final int REQUEST_CODE = 101; private StorageReference videoRef; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_signed_in); String uid = FirebaseAuth.getInstance().getCurrentUser().getUid(); StorageReference storageRef = FirebaseStorage.getInstance().getReference(); videoRef = storageRef.child("/videos/" + uid + "/userIntro.3gp"); } . . }
With the storage reference initialized, add the upload() method to the class as follows:
public void upload(View view) { if (videoUri != null) { UploadTask uploadTask = videoRef.putFile(videoUri); uploadTask.addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Toast.makeText(SignedInActivity.this, "Upload failed: " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); } }).addOnSuccessListener( new OnSuccessListener<UploadTask.TaskSnapshot>() { @Override public void onSuccess(UploadTask.TaskSnapshot taskSnapshot) { Toast.makeText(SignedInActivity.this, "Upload complete", Toast.LENGTH_LONG).show(); } }).addOnProgressListener( new OnProgressListener<UploadTask.TaskSnapshot>() { @Override public void onProgress(UploadTask.TaskSnapshot taskSnapshot) { } }); } else { Toast.makeText(SignedInActivity.this, "Nothing to upload", Toast.LENGTH_LONG).show(); } }
The code uses the putFile() method of the storage reference object to initiate the file upload and then uses listener methods to receive updates on the upload progress. A Toast message is displayed to notify the user of the success or failure of the upload.
Updating the Progress Bar
As the file is uploaded, the progress bar needs to be updated to visually convey to the user the progress being made. Clearly the code to track progress needs to be placed within the onProgress() listener callback method which will be called at regular intervals during the upload process. To calculate this value, the number of bytes uploaded so far needs to be compared to the total size of the file and the resulting value used as the current setting for the ProgressBar. As described in earlier chapters, each time the onProgress() method is called it is passed an UploadTask.TaskSnapshot object from which a variety of properties can be extracted, including the file size and current number of bytes transferred.
When the user interface layout was designed, the ProgressBar was configured with a range of 0 to 100, allowing the current position to be specified as a percentage. With this in mind, create a new method named updateProgress() which takes as an argument a task snapshot object:
public void updateProgress(UploadTask.TaskSnapshot taskSnapshot) { @SuppressWarnings("VisibleForTests") long fileSize = taskSnapshot.getTotalByteCount(); @SuppressWarnings("VisibleForTests") long uploadBytes = taskSnapshot.getBytesTransferred(); long progress = (100 * uploadBytes) / fileSize; ProgressBar progressBar = (ProgressBar) findViewById(R.id.pbar); progressBar.setProgress((int) progress); }
Next, edit the onProgress() callback method to call this method, passing through the current task snapshot:
. . @Override public void onProgress(UploadTask.TaskSnapshot taskSnapshot) { updateProgress(taskSnapshot); } . .
Downloading the Video File
The final coding task is to implement the download() method, the purpose of which is to download the video file from cloud storage using the storage reference and play it back to the user on the VideoView object. The download is started via a call to the getFile() method of the storage reference object, passing through a File instance initialized with a temporary local file. Listeners are also used to monitor the status of the download:
public void download(View view) { try { final File localFile = File.createTempFile("userIntro", "3gp"); videoRef.getFile(localFile).addOnSuccessListener( new OnSuccessListener<FileDownloadTask.TaskSnapshot>() { @Override public void onSuccess( FileDownloadTask.TaskSnapshot taskSnapshot) { Toast.makeText(SignedInActivity.this, "Download complete", Toast.LENGTH_LONG).show(); final VideoView videoView = (VideoView) findViewById(R.id.videoView); videoView.setVideoURI(Uri.fromFile(localFile)); videoView.start(); } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Toast.makeText(SignedInActivity.this, "Download failed: " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); } }); } catch (Exception e) { Toast.makeText(SignedInActivity.this, "Failed to create temp file: " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); } }
== Setting the Storage Security Rules ==
The final task before testing the project is to configure the security rules. Since the storage path includes the user’s authentication ID, this can be used to ensure that the ID of the user seeking access matches that contained in the storage reference path. Access will also be further restricted to enable reading and writing only when the filename matches userIntro.3gp.
Open the Firebase console in a browser window, open the Firebase Examples project and navigate to the Storage screen. Select the Rules tab and enter the following rules:
service firebase.storage { match /b/{bucket}/o { match /videos/{userId}/userIntro.3gp { allow read; allow write: if request.auth.uid == userId; } } }
Click on the Publish button to commit the rules for the project ready for testing.
Testing the Project
Compile and run the app and sign in if prompted to do so. Record some video and tap the button to accept the recording. Upload the video to cloud storage and verify that the progress bar is updated regularly until the upload completes. Tap the download button and wait for the video file to download and begin playing in the video view.
Within the Storage section of the Firebase console, select the Files tab and navigate to the /videos/<uid> folder where the userIntro.3gp video file should now be listed:
Figure 35‑6
Summary
This chapter has created an Android app that makes use of Firebase Cloud Storage to store a video file. The example has demonstrated the use of storage references, file uploading and downloading, the use of listeners to track file transfer progress and the implementation of storage rules to restrict storage file access.