Assignment 10

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

Topics of discussion

  • Offline persistence of data

  • Firebase offline capabilities

  • Determining a user's online presence

Offline persistence of data

Smartphones aren't always connected to the internet (Wifi or data connection), but users expect most of their apps to still maintain functionality, even if limited. For instance, a user will want to read his Yahoo emails even is he is in roaming, or she might want to remember her sightseeing to-do notes from Evernote when hiking up in the mountains. Being disconnected from the Internet means no new data can be retrieved from a possible server, and no data may be synced to the cloud, however, a user should always be able to manipulate the data he has on his phone.

To store persistent data, we will make use of binary files. The folder structure can differ based on the type of the application, but by default, we can consider that our app manages a list of objects, which are shown and edited by the user in a Listview. This list may be serialized to a list of files, each file corresponding to an object in the list, all placed directly in the app's default file directory. To dump object instances to a file, we should implement a method in the AppState similar to the one below. Consider we have objects of type Pizzeria, uniquely identified by a String called name.

public void savePizzeriaToFile(Context context, Pizzeria pizzeria) {

String fileName = pizzeria.name;

try {

FileOutputStream fos = context.openFileOutput(fileName, Context.MODE_PRIVATE);

ObjectOutputStream os = new ObjectOutputStream(fos);

os.writeObject(pizzeria.copy());

os.close();

fos.close();

} catch (IOException e) {

Toast.makeText(context, "Local data may not be accessed. An internet connection is required", Toast.LENGTH_SHORT).show();

return;

}

}

The method above first determines the new file name (pizzeria's name), then invokes openFileOutput and writeObject. To load all pizzeria's back into memory, we can use a method call like the one below:

public List<Pizzeria> loadAllPizzeriasFromFile(Context context) {

try {

List<Pizzeria> pizzerias = new ArrayList<>();

for (File file : context.getFilesDir().listFiles()) {

FileInputStream fis = context.openFileInput(file.getName());

ObjectInputStream is = new ObjectInputStream(fis);

Pizzeria pizzeria = (Pizzeria) is.readObject();

pizzerias.add(pizzeria);

is.close();

fis.close();

}

return pizzerias;

} catch (IOException e) {

Toast.makeText(context, "Local data may not be accessed. An internet connection is required", Toast.LENGTH_SHORT).show();

} catch (ClassNotFoundException e) {

e.printStackTrace();

}

return null;

}

In order for an instance of Pizzeria (or any other object) to be saved in binary format, a procedure called marshaling occurs. While the low-level operations of mapping the class members to and back from binary data run in the background, the class must ensure it is serializable and it implements Serializable. By default, any custom defined class is serializable, except when it uses compound or complex data. In that case you have to ensure the compound data is further serializable. For example, if we look back at the code example for savePizzeriaToFile, notice a copy of pizzeria is passed (i.e. os.writeObject(pizzeria.copy()) ). A copy of that instance is created at runtime because the Pizzeria class contains non-serializable members which is set by default to null by the copy method. When the instance is loaded back, you must make sure the null pointers are treated accordingly. In this particular example, one such member is a ParseFile, namely a logo of the restaurant to be retrieved from a server. When the app is offline, and the logo is set to null, the app should be able to load a default icon for all pizzerias.

The last problem is that of determining how to synchronize the local copy of the data snapshot. Let's consider we have an online version of the data, named D1. To make D1 always available on the phone, we always back it up locally, as D2. Ideally, if there is a connection, D2 = D1. However, if the connection is up and we work directly with the server (making writes), then D2 will become obsolete; as such, D2 needs to always be refreshed with the current version of D1 on the server. On the other hand, if the connection is lost, then we start altering D2, so that when we come back online, D1 will be in an older state than D2, so we need to update the server-side database. Finally, what happens when we connect with different clients to the same D1 database, and have local copies of that data, all in different states? There are three solutions that can be employed:

  • Local copy overwrites online database (D2>D1)

  • Server-side snapshot always overwrites local backup (D1>D2)

  • Update server snapshot with most recent entries, then update all other clients from server ( max(D1, D2) ). For this strategy, all objects stored in the database need to have a timestamp when they were last modified.

Firebase offline capabilities

Firebase apps work great offline and we have several features to make the experience even better. Enabling disk persistence allows your app to keep all of its state even after an app restart. We provide several tools for monitoring presence and connectivity state.

Disk Persistence

Firebase apps automatically handle temporary network interruptions for you. Cached data will still be available while offline and your writes will be resent when network connectivity is recovered. Enabling disk persistence allows our app to also keep all of its state even after an app restart. We can enable disk persistence with just one line of code. With disk persistence enabled, our synced data and writes will be persisted to disk across app restarts and our app should work seamlessly in offline situations.

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

The Firebase Realtime Database client automatically keeps a queue of all write operations that are performed while our application is offline. When persistence is enabled, this queue will also be persisted to disk so all of our writes will be available when we restart the app. When the app regains connectivity, all of the operations will be sent to the server.

Firebase synchronizes and stores by default only a local copy of the data for active listeners. In addition, you can keep specific locations in sync. The client will automatically download the data at these locations and keep it in sync even if the reference has no active listeners. You can turn synchronization back off with the following line of code. By default, 10 MB of previously synced data will be cached. This should be enough for most applications. If the cache outgrows its configured size, Firebase will purge data that has been used least recently. Data that is kept in sync, will not be purged from the cache. The database reference path can be specified explicitly with the child method.

DatabaseReference monthReference = FirebaseDatabase.getInstance().getReference().child("calendar").child("january");

// turn sync on

monthReference.keepSynced(true);

// turn sync off

monthReference.keepSynced(false);

Any transactions that are performed while your app is offline, will be queued. Once the app regains network connectivity, the transactions will be sent to the server. Even with persistence enabled, transactions are not persisted across app restarts. So you cannot rely on transactions done offline being committed to your Firebase database. To provide the best user experience, your app should show that a transaction has not been saved to Firebase yet, or make sure your app remembers them manually and executes them again after an app restart.

Task #1

  • Based on the Smart Wallet application from last week, implement the functionality to make a local backup of the list of payments your receive from Firebase. Consequently, the app should run without internet (at least in read-mode). Any changes to payments should be updated locally, and when the connection to the server is reestablished, the locally updated payments should be synchronized with Firebase. Any time there is a connection to the server and data is changed, make sure you also update the local copy of the data.

  • For this, first ensure the Payment class implements Serializable and also is serializable.

  • Create two helper methods in the AppState class: updateLocalBackup(Context context, Payment payment, boolean toAdd) and loadFromLocalBackup(Context context, String currentMonth).

                • public void updateLocalBackup(Context context, Payment payment, boolean toAdd) {

                • String fileName = payment.timestamp;

                • try {

                • if (toAdd) {

                • // save to file

                • } else {

                • context.deleteFile(fileName);

                • }

                • } catch (IOException e) {

                • Toast.makeText(context, "Cannot access local data.", Toast.LENGTH_SHORT).show();

                • }

                • }

                • public boolean hasLocalStorage(Context context) {

                • return context.getFilesDir().listFiles().length > 0;

                • }

                • public List<Payment> loadFromLocalBackup(Context context, String month) {

                • try {

                • List<Payment> payments = new ArrayList<>();

                • for (File file : context.getFilesDir().listFiles()) {

                • if (/*only own files*/) {

                • // ...

                • if (/* current month only */)

                • payments.add(payment);

                • }

                • }

                • return payments;

                • } catch (IOException e) {

                • Toast.makeText(context, "Cannot access local data.", Toast.LENGTH_SHORT).show();

                • } catch (ClassNotFoundException e) {

                • e.printStackTrace();

                • }

                • return null;

                • }

  • In the onCreate method of your MainActivity make sure you check is network is available. If it is, everything should proceed as before; else, check if there is a local backup of data, then load the filtered list of payments from local storage using the above implemented method.

                • if (!AppState.isNetworkAvailable(this)) {

                • // has local storage already

                • if (AppState.get().hasLocalStorage(this)) {

                • payments = /**/

                • tStatus.setText("Found " + payments.size() + " payments for " + Month.intToMonthName(currentMonth) + ".");

                • } else {

                • Toast.makeText(this, "This app needs an internet connection!", Toast.LENGTH_SHORT).show();

                • return;

                • }

                • }

                  • public static boolean isNetworkAvailable(Context context) {

                  • ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(context.CONNECTIVITY_SERVICE);

                  • NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();

                  • return activeNetworkInfo != null && activeNetworkInfo.isConnected();

                  • }

    • If the connection is available, then you should make sure all local data is kept in sync with the server snapshot. As such, update the local data in all methods of the listener attached to Firebase (add, update, remove).

    • Make sure the local data is synced with the operations of creating, updating or deleting payments from the AddPaymentActivity.

  • Homework (hard): Create your own API to handle all reads and writes to/from the database or local backup transparently. Provide appropriate methods for writing and retrieving data from the unified storage by wrapping the calls to local/online snapshots. Also, keep in mind to synchronize the two snapshots based on timestamps of the data. As a suggestion, each payment should have a timestamp when it was last modified. 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.

Balsam fruit

Indigenous to the tropical regions of Africa, Momordica balsamina is a tendril-bearing annual vine. The Bright Orange apple unlike the usual apples have numerous, glossy and sticky seeds. This bitter yet extremely health benefited fruit is sometimes sold in the Asian markets as a vegetable. The bitterness is tackled by blanching or cooking the fruit but is not eradicated completely. Also known as the Bitter gourd, sometimes most of the bitterness can be removed if soaked in water or in warm salt water. Smell-wise, they do not smell great, more like an old gym shoe.

http://www.fruitsinfo.com/balsam-apple-fruit.php