Implement Marker Clustering in the Open Event Android App

Markers are an integral part of any map based service. In the Open Event Android App for samples like Mozilla All Hands 2017, there are a lot of microlocations that the organizers want to integrate into the app’s map fragment. Due to the presence of large number of markers, the map fragment clutters, thereby harming the user experience. As an example, imagine yourself as the user and you see the map as in the image given below!

Therefore to tackle problem like this, the markers are grouped into clusters. On click of the cluster, the markers get declustered and fall into their respective locations with the map zoomed in.

Implementation

First and foremost, define the libraries to be used by the utilities in the build.gradle of your app module. Make to import the latest versions.

// Googleplay Variant
googleplayCompile 'com.google.android.gms:play-services-maps:10.2.6'
googleplayCompile 'com.google.android.gms:play-services-location:10.2.6'
googleplayCompile 'com.google.maps.android:android-maps-utils:0.4'

 

Implement the ClusterItem interface in your location POJO which will house a marker’s location. The POJO will therefore override the getPostion() method of the ClusterItem interface where you will return the LatLng.

public class MicrolocationClusterWrapper implements ClusterItem {

@Override
public LatLng getPosition() {
   return latLng;
}

}

 

Create a custom Cluster Renderer class that will extend the default cluster renderer with you location POJO as parameter. Implement ClusterManager’s onClusterItemClickListener to listen to marker clicks and add custom colors to them. Set the custom marker properties before the marker items are rendered with the markerOptions inside the onBeforeClusterItemRendered().

@Override
   protected void onBeforeClusterItemRendered(MicrolocationClusterWrapper item, MarkerOptions markerOptions) {
       super.onBeforeClusterItemRendered(item, markerOptions);

       markerOptions.title(item.getMicrolocation().getName());
       if (microlocationClusterWrapper != null && item.equals(microlocationClusterWrapper)) {
           markerOptions.icon(ImageUtils.vectorToBitmap(context, R.drawable.map_marker, R.color.color_primary));
       } else {
           markerOptions.icon(ImageUtils.vectorToBitmap(context, R.drawable.map_marker, R.color.dark_grey));
       }
   }

   @Override
   protected void onClusterItemRendered(final MicrolocationClusterWrapper clusterItem, Marker marker) {
       super.onClusterItemRendered(clusterItem, marker);
       clusterItem.setMarker(marker);
  }

   @Override
   public boolean onClusterItemClick(MicrolocationClusterWrapper item) {
       if (microlocationClusterWrapper != null) {
           getMarker(microlocationClusterWrapper).setIcon(ImageUtils.vectorToBitmap(context, R.drawable.map_marker, R.color.dark_grey));
       }
       microlocationClusterWrapper = item;
       getMarker(item).setIcon(ImageUtils.vectorToBitmap(context, R.drawable.map_marker, R.color.color_primary));
       return false;
   }
}

 

Finally in your map fragment, initialize your map, cluster manager class and your custom cluster renderer you just created. Implement the MapReadyCallback so that the Google Map object is not null. Remember to pass the cluster renderer as a listener for the cluster manager’s cluster item click listener. Use the setOnClusterClickListener to zoom the map on the click of cluster.

private void handleClusterEvents() {
   clusterManager.setOnClusterItemClickListener(clusterRenderer);

   clusterManager.setOnClusterClickListener(cluster -> {
               mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(
                       cluster.getPosition(), (float) Math.floor(mMap
                               .getCameraPosition().zoom + 2)), 300,
                       null);

               return true;
           });

   mMap.setOnMapClickListener(clusterRenderer);
}

 

Conclusion

Maps are an integral part of any event based apps and marker clustering undoubtedly enhances the user experience in Maps.

Resources

  • Marker Clustering Android documentation

https://developers.google.com/maps/documentation/android-api/utility/marker-clustering

  • Complete Code Reference

https://github.com/fossasia/open-event-android/pull/1777

  • Marker Customization in the case of Clustering

https://github.com/googlemaps/google-maps-ios-utils/issues/21

Implement Caching in the Live Feed in the Open Event Android App

In the Open Event Android App, a live feed from the event’s Facebook page was recently implemented. Since it was a live feed, it was decided that it was futile to store it in the Realm database of the app. The data of the live feed didn’t persist anywhere, hence the feed used to be empty when the app ran without the internet connection.

To solve the problem of data persistence, it was decided to store the feed in the cache. Now, there were two paths before us – use retrofit okhttp cache management or use volley. Since retrofit is used to make the API requests in the app, we used the former. To implement caching with retrofit, its API response should include the cache control header. Since it was not a response generated by a personal server, interceptors were needed to force change the request.

Interceptors

Interceptors are a powerful mechanism that can monitor, rewrite, and retry calls. The solution was to use interceptors to rewrite the calls to force use of cache. Two interceptors were added, application interceptor for the request and the network interceptor for the response.

Implementation

Create a cache file to store the response.

private static Cache provideCache() {
   Cache cache = null;
   try {
       cache = new Cache(new File(OpenEventApp.getAppContext().getCacheDir(), "facebook-feed-cache"),
               10 * 1024 * 1024); // 10 MB
   } catch (Exception e) {
       Timber.e(e, "Could not create Cache!");
   }
   return cache;
}

 

Create a network interceptor by chaining the response with the cache control header and removing the pragma header to force use of cache.

private static Interceptor provideCacheInterceptor() {
   return chain -> {
       Response response = chain.proceed(chain.request());

       // re-write response header to force use of cache
       CacheControl cacheControl = new CacheControl.Builder()
               .maxAge(2, TimeUnit.MINUTES)
               .build();

       return response.newBuilder()
               .removeHeader("Pragma")
               .header(CACHE_CONTROL, cacheControl.toString())
               .build();
   };
}

 

Create an application interceptor by chaining the request with the cache control header for stale responses and removing the pragma header to make the feed available for offline usage.

private static Interceptor provideOfflineCacheInterceptor() {
   return chain -> {
       Request request = chain.request();

       if (!NetworkUtils.haveNetworkConnection(OpenEventApp.getAppContext())) {
           CacheControl cacheControl = new CacheControl.Builder()
                   .maxStale(7, TimeUnit.DAYS)
                   .build();

           request = request.newBuilder()
                   .removeHeader("Pragma")
                   .cacheControl(cacheControl)
                   .build();
       }

       return chain.proceed(request);
   };
}

 

Finally add the cache and the two interceptors while building the okhttp client.

OkHttpClient okHttpClient = okHttpClientBuilder.addInterceptor(new HttpLoggingInterceptor()
       .setLevel(HttpLoggingInterceptor.Level.BASIC))
       .addInterceptor(provideOfflineCacheInterceptor())
       .addNetworkInterceptor(provideCacheInterceptor())
       .cache(provideCache())
       .build();

 

Conclusion

Working of apps without the internet connection builds up a strong case for corner cases while testing. It is therefore critical to persist data however small to avoid crashes and bad user experience.

Resources

  • Complete code reference

https://github.com/fossasia/open-event-android/pull/1750

  • Tutorial on etags in response

https://futurestud.io/tutorials/retrofit-2-activate-response-caching-etag-last-modified

  • Tutorial on caching in Retrofit

https://caster.io/episodes/retrofit-2-offline-cache/

  • Retrofit Documentation

http://square.github.io/retrofit/

  • Okhttp Interceptors Documentation

https://github.com/square/okhttp/wiki/Interceptors

 

 

 

 

 

 

 

 

Create an AutocompleteTextView dropdown for the email input in the Open Event Orga Android App

In the first version of the Open Event Organizer App, the event organizer was required to enter his full email each time he logged out of his account and therefore it was hindering the user experience. AutoCompleteTextView with shared preferences is a solution to this problem. This feature provides an editable text view that shows completion suggestions automatically while the user is typing. The list of suggestions is displayed in a drop down menu. The user can choose an item to replace the content of the edit box with. It is extremely useful in enhancing user experience.

The solution we implemented was to create an autocomplete textview for the email input, store the email address of the user on a successful login in the shared preference in a set of strings to prevent duplicacy and display it in the dropdown on subsequent login attempts.

Implementation

Change your TextInputLayout structure to accommodate the autocompletetextview. Remember to create a separate autocompletetextview object with the specific id of the view.

<android.support.v7.widget.AppCompatAutoCompleteTextView
       android:id="@+id/email_dropdown"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:hint="@string/email"
       android:inputType="textEmailAddress" />

 

Create Utility methods to get/store the emails in the shared preferences. The set data structure has been used here so that there is no duplicacy while storing the emails in the shared preferences.

public Set<String> getStringSet(String key, Set<String> defaultValue) {
   return sharedPreferences.getStringSet(key, defaultValue);
}

public void saveStringSet(String key, Set<String> value) {
   SharedPreferences.Editor editor = sharedPreferences.edit();
   editor.putStringSet(key, value);
   editor.apply();
}

public void addStringSetElement(String key, String value) {
   Set<String> set = getStringSet(key, new HashSet<>());
   set.add(value);
   saveStringSet(key, set);
}

 

Create helper methods to add an email and retrieve the list of emails from the shared preferences to provide it to the views.

private void saveEmail(String email) {
   utilModel.addStringSetElement(Constants.SHARED_PREFS_SAVED_EMAIL, email);
}

private Set<String> getEmailList() {
   return utilModel.getStringSet(Constants.SHARED_PREFS_SAVED_EMAIL, null);
}

 

Create an autocompleteTextView object in your activity with the help of the R id from the layout and set the adapter with the set of strings retrieved from the shared preferences. You could create a custom adapter for this case too, but as far as the Open Event Orga App was concerned, using the array adapter made sense.

autoCompleteEmail.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1,
   new ArrayList<String>(emails)));

 

Conclusion

It is important that the user is served with the best possible experience of the application and the autocomplete text view for the email serves just that.

Resources

Generate a Live feed from Facebook for the Open Event Android App

To increase the user engagement in the Open Event Android app, the feed was a welcome feature for the users. In order to fetch data from a public facebook page, firstly the facebook page id should be fetched from an API GET request. To fetch the page id, the page name have to be included in the API request along with the access token generated from the facebook developers page itself.

To fetch the data from Facebook, it requires the user to login to Facebook. Facebook requires its SDK to be set up first on the android itself to make the requests. Hence it destroys the motive to have feed of a public Facebook page.

Solution

The solution involves making custom API requests to the Facebook servers in order to fetch the data. 3 API requests have to be made. The first fetches the access token for the developer to access the facebook servers for the data. It’s going to be a one time call, so just make the API request in the browser itself and fetch the token with the help of the app ID and the app secret. The second fetches the page id of a particular facebook page from the given facebook page name. The third fetches the data using the page id with the fields specified in the query. The API requests will be made with the help of the retrofit library and the object communication throughout the app will be catered by rxJava library.

Implementation

Create a Facebook developer account. Generate the app ID for your app along with its secret.

Make a GET request to fetch the access token from the app ID and app secret.

https://graph.facebook.com/oauth/access_token?client_id={APP_ID}&client_secret={APP_SECRET}&grant_type=client_credentials

#NOTE: Remove the brackets!

Make the appropriate JSON getters and setters of the feed from the GET request. Use http://www.jsonschema2pojo.org/ to autogenerate them with the help of a JSON input.

Create the Facebook Graph API interface to make the GET requests. Make them observable for object communication using rxJava.

public interface FacebookGraphAPI {

[email protected](“/{event_name}”)
  Observable<FacebookPageId> getPageId(@Path(“event_name”) String eventName, @Query(“access_token”) String accessToken);

[email protected](“/{page_id}/feed”)
  Observable<Feed> getPosts(@Path(“page_id”) String pageId, @Query(“fields”) String fields, @Query(“access_token”) String accessToken);

}

Create an API client to build the retrofit along with the OkHttpClient

public final class APIClient {

  private static final int CONNECT_TIMEOUT_MILLIS = 20 * 1000; // 15s

  private static final int READ_TIMEOUT_MILLIS = 50 * 1000; // 20s

  private static FacebookGraphAPI facebookGraphAPI;

  private static Retrofit.Builder retrofitBuilder;

  static {
      OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient().newBuilder()
              .connectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
              .readTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
      if (BuildConfig.DEBUG)
          okHttpClientBuilder.addNetworkInterceptor(new StethoInterceptor());
      OkHttpClient okHttpClient = okHttpClientBuilder.addInterceptor(new HttpLoggingInterceptor()
              .setLevel(HttpLoggingInterceptor.Level.BASIC))
              .build();

      retrofitBuilder = new Retrofit.Builder()
              .addConverterFactory(JacksonConverterFactory.create(new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)))
              .client(okHttpClient);
  }

  public static FacebookGraphAPI getFacebookGraphAPI() {
      if (facebookGraphAPI == null)
          facebookGraphAPI = retrofitBuilder
                  .baseUrl(Urls.FACEBOOK_BASE_URL)
                  .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                  .build()
                  .create(FacebookGraphAPI.class);
      return facebookGraphAPI;
  }

}

Create a utility method to get the relative time from the timestamp of the post.

private static final Locale defaultLocale = Locale.getDefault();
private static String iso8601WithTimezone = “yyyy-MM-dd’T’HH:mm:ssZ”;
private static final String ISO_TIMEZONE_FORMATTER = new SimpleDateFormat(iso8601WithTimezone, defaultLocale);

@NonNull
private static Date getDate(@NonNull SimpleDateFormat formatter, @NonNull String isoDateString) throws ParseException {
  return formatter.parse(isoDateString);
}

public static String getRelativeTimeFromTimestamp(String timeStamp) throws ParseException {
  Date timeCreatedDate = getDate(ISO_TIMEZONE_FORMATTER, timeStamp);

  return (String) android.text.format.DateUtils.getRelativeTimeSpanString(
          (timeCreatedDate.getTime()),
          System.currentTimeMillis(), android.text.format.DateUtils.SECOND_IN_MILLIS);
}

Create a feed fragment, download and subscribe to the feed with the help of the retrofit and rxjava library. Notify the adapter when the data set changes.

private void downloadFeed() {
  APIClient.getFacebookGraphAPI()
          .getPosts(sharedPreferences.getString(ConstantStrings.FACEBOOK_PAGE_ID, null),
                  getContext().getResources().getString(R.string.fields),
                  getContext().getResources().getString(R.string.facebook_access_token))
          .subscribeOn(Schedulers.io())
          .observeOn(AndroidSchedulers.mainThread())
          .subscribe(feed -> {
              feedItems.clear();
              feedItems.addAll(feed.getData());
              feedAdapter.notifyDataSetChanged();
              handleVisibility();
          }, throwable -> {
              Snackbar.make(swipeRefreshLayout, getActivity()
                      .getString(R.string.refresh_failed), Snackbar.LENGTH_LONG)
                      .setAction(R.string.retry_download, view -> refresh()).show();
              Timber.d(“Refresh not done”);
          }, () -> {
              swipeRefreshLayout.setRefreshing(false);
              Timber.d(“Refresh done”);
          });
}

Conclusion

The open event android project currently only fetches feed from the facebook. Feeds from twitter and youtube will be fetched too in the near future. Watch out the next blog on how to display the comments of Facebook post on a dialogfragment over the feed fragment.

For more information on the code, please take help from here.

Iteration through the Android File System in the phimpme project

Android uses the single file system structure which has a single root. The task involved creating a custom folder chooser to whitelist folders while displaying images in the gallery in the Phimpme Photo App. The challenge arose in iterating over the files in the most efficient way. The best possible way to represent the file structure is in the form of tree data structure as given below.

Current Alternative

Currently, the MediaStore class contains metadata for all available media on both internal and external storage devices. Since it only returns a list of a particular media file format, it refrains the developer from customizing the structure in his way.

Implementation

Create a public class which represents the file tree. Since each subtree of the tree could itself be represented as file tree itself, therefore the parent of a node will be a FileTree object itself. Therefore declare a list of FileTree objects as children of the node, a FileTree object as the parent of the particular node, node’s own File object along with string values filepath and display name associated with it.

public class FileTree {
 public final String filepath;
 public final String displayName;
 public final List<FileTree> childFileTreeList = new ArrayList<>();
 public final FileTree parent;
 public boolean hasMedia = false;

 public FileTree(String filepath, String displayName, FileTree parent) {
    this.filepath = filepath;
    this.displayName = displayName;
    this.parent = parent;
 }
}

For iterating through the file system, we create a recursive function which is called on the root of the Android file system. If the particular file is a directory, with the help of Depth First traversal algorithm, the directory is traversed. Else, the file is added to the list of the file. The below code snippet is the recursive function.

public static void walkDir(FileTree fileTree, List<File> files) {
  File listFile[] = fileTree.file.listFiles();

  if (listFile != null) {
      for (File file : listFile) {
          if (file.isDirectory()) {
              FileTree childFileTree = new FileTree(file, file.getName(), fileTree);
              fileTree.childFileTreeList.add(childFileTree);
              walkDir(childFileTree, files);
          }
          else {
                  files.add(file);

          }
      }
  }
}

Conclusion

The android file system was used to whitelist folders so that the images of the folders could neither be uploaded nor edited.

For the complete guide to whitelisting folders, navigate here

Allow users to change language in the Open Event Android app via settings

Localisation of the application is a major step towards maximizing market penetration especially in cases like that of India which houses about 22 official languages. Therefore it is important to allow users to change their language preference from the Open Event Android App itself. There are two different ways through which this could be implemented.

  1. Navigate the user to the phone settings page by firing an implicit intent.
  2. Build an in-app language chooser specific to the country and prompt user to choose language on the first launch of the application.

Out of the two, the open event developers community along the mentors decided to go with the first option since it seemed more apt.

Implementation

In order to implement this, the first and foremost requirement is to keep culture-specific resources separated from the rest of your app. Android resolves language and culture-specific resources based on the system locale setting. Support is provided for different locales by using the resources directory in the Android project. The resources especially the string resources have their own language specific string.xml files and they get resolved with the help of key-value pair mapping. The following steps implement it –

Create a static variable in the application class of the app and assign it the default display language. Make sure to override OnConfigurationChanged method to change the language of the app when the user changes the language.

public class OpenEventApp extends Application {

public static String sDefSystemLanguage;

@Override
public void onCreate() {
 super.onCreate();
 
 sDefSystemLanguage = Locale.getDefault().getDisplayLanguage();
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
 super.onConfigurationChanged(newConfig);
 sDefSystemLanguage = newConfig.locale.getDisplayLanguage();
}
}

Include a preference category in your application’s settings.xml which corresponds to the preference activity of the app.

< PreferenceCategory
android: key = “change_language_category”
android: title = “@string/language_preferences” >

< Preference
android: defaultValue = “@string/default_mode_language”
android: key = “@string/language_key”
android: title = “@string/change_language” / >
< /PreferenceCategory>

Finally in your preference activity, set your language preference to the string defined in the application class and implement a click listener on the preference to fire an implicit intent.

public class SettingsActivity extends PreferenceActivity implements Preference.OnPreferenceChangeListener {

private Preference languagePreference;
private SharedPreferences preferences;

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 preferences = PreferenceManager.getDefaultSharedPreferences(this);
 addPreferencesFromResource(R.xml.settings);
 setContentView(R.layout.activity_settings);

 languagePreference = findPreference(LANGUAGE_PREF_MODE);
 languagePreference.setSummary(OpenEventApp.sDefSystemLanguage);
}

@Override
public boolean onPreferenceChange(Preference preference, Object o) {

  if (preference.getKey().equals(getResources().getString(R.string.language_key))) {
  languagePreference.setSummary((String) o);
 }

 return false;
}

 @Override
 public void onResume() {
 super.onResume();
 languagePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
  @Override
  public boolean onPreferenceClick(Preference preference) {
   startActivity(new Intent((Settings.ACTION_LOCALE_SETTINGS)));
   return true;
  }
 });

 languagePreference.setSummary(OpenEventApp.sDefSystemLanguage);
}

}

Conclusion

Language can be the greatest asset for an app to reach maximum market capitalisation. It is therefore critical for the apps like open event android to reach scalability for the events to have maximum outreach.

Enhance the Map Fragment in the Open Event Android app

The usual map fragment in the Open Event Android App didn’t match to the other UI enhancements in the app. Therefore it was needed to enhance it in order for the users to take full advantages of the map based services that the app offered. The enhancements primarily included the following features –

  • Better marker drawables – The usual markers were dull and didn’t match the app theme of the rest of the app.
  • Location searching with autocomplete dropdown – Initially, there was no way the user could search for the location of the sessions happening in the event. Also, it was decided that it was best if the search auto suggested locations for easy navigation.
  • Navigation to the location marker – The google map class comes bundled with in built UI features such as floor recognition, location pointer etc. Navigation to the location is one of those features which appears on a marker click. The user gets directed to the Google Maps android application for navigation from the current location.

Implementation

Add the following dependencies to your app’s Gradle build file. Do update the libraries if a newer version is available.

dependencies {
  compile ‘com.google.maps.android:android-maps-utils:0.4+’
  compile ‘com.google.android.gms:play-services-location:8.1.0’
  compile ‘com.google.android.gms:play-services-maps:8.1.0’
}

Use vector drawables instead of raster icons for the marker

<!– drawable/map_marker.xml –>
<vector xmlns:android=“http://schemas.android.com/apk/res/android”
  android:height=“40dp”
  android:width=“40dp”
  android:viewportWidth=“40”
  android:viewportHeight=“40”>
  <path android:fillColor=“#000” android:pathData=“M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z” />
</vector>

Customize the marker events including color selections onclick and title display.

private Marker handleMarkerEvents(LatLng location, String locationTitle) {
  if (mMap != null) {
      locationMarker = mMap.addMarker(new MarkerOptions().position(location).title(locationTitle)
              .icon(vectorToBitmap(getContext(), R.drawable.map_marker, R.color.dark_grey)));
      mMap.setOnMarkerClickListener(marker -> {
          locationMarker.setIcon(vectorToBitmap(getContext(), R.drawable.map_marker, R.color.dark_grey));
          locationMarker = marker;
          marker.setIcon(vectorToBitmap(getContext(), R.drawable.map_marker, R.color.color_primary));
          return false;
      });
      mMap.setOnMapClickListener(latLng -> locationMarker.setIcon(
              vectorToBitmap(getContext(), R.drawable.map_marker, R.color.dark_grey)));
  }
  return locationMarker;
}

//Vector currently cannot be directly used with the markers hence it is necessary to convert it into bitmap first

private BitmapDescriptor vectorToBitmap(Context context, @DrawableRes int id, int color) {
  Drawable vectorDrawable = ResourcesCompat.getDrawable(context.getResources(), id, null);
  assert vectorDrawable != null;
  Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
          vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
  Canvas canvas = new Canvas(bitmap);
  vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
  DrawableCompat.setTint(vectorDrawable, ContextCompat.getColor(context, color));
  vectorDrawable.draw(canvas);
  return BitmapDescriptorFactory.fromBitmap(bitmap);
}

Enable the google maps UI enhancements by getting the UI settings and subsequently setting true to google map toolbar

mMap.getUiSettings().setMapToolbarEnabled(true);

Conclusion

The google maps is an important service for the open event android project since events are all about happenings and happenings are best served with accurate locations of their whereabouts.

For more information on the code, please visit the link here.

Remove redundancy of images while uploading it from the Phimpme photo app

Uploading images is an important feature for the Phimpme Photo App. While uploading images in the Phimpme application, there was a high probability that the user uploaded redundant images since there was no check on the upload. Therefore to tackle this problem, images should be hashed so that no two same images gets uploaded. To generate a hash of the file, the md5 algorithm and the sha1 algorithm is used.

What is MD5 algorithm?

The MD5 hashing algorithm is a one-way cryptographic function that accepts a message of any length as input and returns as output a fixed-length digest value to be used for authenticating the original message. It produces a 128 bit hash value. 

WhatIs.com

What is SHA1 algorithm?

SHA-1 (Secure Hash Algorithm 1) is a cryptographic hash function designed by the United States National Security Agency and is a U.S. Federal Information Processing Standard published by the United States NIST. SHA-1 produces a 160-bit (20-byte) hash value known as a message digest. A SHA-1 hash value is typically rendered as a hexadecimal number, 40 digits long.  

 – Wikipedia

Reason for using MD5 + SHA1 instead of SHA256

  1. The file becomes highly secured while maintaining its integrity.
  2. The algorithm MD5 + SHA1 is faster in hashing than SHA256.

Implementation

Create a separate Utils class named FileUtils.java. The class contains the static function to get the hash of any file in general. The message digest class is being used in both the algorithms.

The hash is the combination of the md5 hash string along with the SHA1 hash string. The message digest takes up algorithm as its argument to specify which hashing function would be used. The fileinputstream takes up the input file for initialization and the message digest is updated with the data bytes of the file. The StringBuffer class is then used to generate the fixed length hexadecimal value from the message digest since the string is going to be mutable in nature. The string value of the generated string buffer object is returned.

public class FileUtils {

//The utility method to get the hash of the file
 public static String getHash(final File file) throws NoSuchAlgorithmException, IOException {

  return md5(file) + "_" + sha1(file);
 }

//The md5 alogorithm
 public static String md5(final File file) throws NoSuchAlgorithmException, IOException {

  MessageDigest md = MessageDigest.getInstance("MD5");
  FileInputStream fis = new FileInputStream(file);
  byte[] dataBytes = new byte[1024];
  int nread = 0;
  while ((nread = fis.read(dataBytes)) != -1) {
   md.update(dataBytes, 0, nread);
  }

  //convert the byte to hex format
  StringBuffer sb = new StringBuffer("");
  for (byte mdbyte: md.digest()) {
   sb.append(Integer.toString((mdbyte & 0xff) + 0x100, 16).substring(1));
  }

  fis.close();
  md.reset();
  return sb.toString();
 }

//The SHA1 algorithm
 public static String sha1(final File file) throws NoSuchAlgorithmException, IOException {

  MessageDigest md = MessageDigest.getInstance("SHA1");
  FileInputStream fis = new FileInputStream(file);
  byte[] dataBytes = new byte[1024];
  int nread = 0;
  while ((nread = fis.read(dataBytes)) != -1) {
   md.update(dataBytes, 0, nread);
  }

  //convert the byte to hex format
  StringBuffer sb = new StringBuffer("");
  for (byte mdbyte: md.digest()) {
   sb.append(Integer.toString((mdbyte & 0xff) + 0x100, 16).substring(1));
  }

  fis.close();
  md.reset();
  return sb.toString();
 }
}

Conclusion

Physical memory is limited. Hence it is of utmost importance that a particular file is identified using a unique identifier in terms of a fixed hash value. Not only will it be beneficial for optimum space utilization, it will also be useful to track it if necessary.

To learn more about the hash alogorithms, visit – https://www.tutorialspoint.com/cryptography/cryptography_hash_functions.htm