Xamarin.Android - Duplicate Image Detector

Introduction

 
This article all about developing real-life mobile applications in Xamarin.Android with the help of a hashing algorithm that will be used for detecting duplicate images from any mobile device with 100% accuracy. Today, I will show you how to build an app that will help you on a daily basis for finding duplicate images from your phone.
 

Why This App?

 
Nowadays, every person has social media apps like WhatsApp, Facebook Messenger, and Instagram. It's common to receive images from contacts but sometimes you may receive the same image from many friends. However, it is very difficult to see images from the gallery one by one and try to find duplicate images to clean your data and storage.
 
Duplicate data is very harmful when you have low memory storage. Under consideration of this problem, I planned to build a duplicate finder that finds the duplicate images for you so you only need to press the delete button if you want to delete them.
 
In this post, I will teach you how to build this app exactly with all frontend and backend-coding. So, let get started
 
In the initial stage, we need all the images of references that we have on our phone. Moreover, put these references to a list of string and implement an algorithm on these references to get hashing of every item. After getting hashing, we will use LINQ to find similar hashes with their references and put these to a new List of string with named
ListOfduplicates.
 
The steps given below are required to be followed in order to create a Duplicate Image Detector app in Xamarin.Android, using Visual Studio.
 
Prerequisites
 
You will be required to install all of these Nuget Packages to build this app.
  1. Xamarin.Android.Support.v7.AppCompat
  2. Xamarin.Android.Support.Design
  3. Xamarin.Android.Support.v7.CardView
  4. Xamarin.Android.Support.v7.RecyclerView
Step 1 - Create an Android Project
 
Create your Android solution in Visual Studio or Xamarin Studio. Select Android and from the list, choose Android Blank App. Give it a name, like DuplicateImageDetector.
(ProjectName: DuplicateImageDetector)
 
Step 2 - Install Nuget Packages
 
After creating a blank project, first, add all of the above-mentioned NuGet packages to this project by right-clicking on References and select manage NuGet packages.
 
Step 3 - User Interface of Application
 
Open Solution Explorer-> Project Name-> Resources-> Layout-> activity_main.axml. Open this main layout file and add the following code. In the layout, we will need the two buttons and RecyclerView holder.
 
(File Name: activity_main.axml , Folder Name: Layout)
  1. <?xml version="1.0" encoding="utf-8"?>    
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    
  3.     xmlns:app="http://schemas.android.com/apk/res-auto"    
  4.     xmlns:tools="http://schemas.android.com/tools"    
  5.     android:orientation="vertical"    
  6.     android:layout_width="match_parent"    
  7.     android:layout_height="match_parent"    
  8.     android:minWidth="25px"    
  9.     android:minHeight="25px">    
  10.     <Button    
  11.         android:id="@+id/btnChoose"    
  12.         android:text="Check Duplicate Images"    
  13.         android:layout_width="match_parent"    
  14.         android:layout_height="wrap_content" />    
  15.     <Button    
  16.         android:id="@+id/btnDelete"    
  17.         android:text="Delete Duplicate Images"    
  18.         android:layout_width="match_parent"    
  19.         android:layout_height="wrap_content" />    
  20.     <android.support.v7.widget.RecyclerView    
  21.         android:layout_width="match_parent"    
  22.         android:layout_height="wrap_content"    
  23.         android:id="@+id/reycyclerViewImages" />    
  24. </LinearLayout>    
Step 4 - Add Item Layout
 
Next, add a new layout by going to Solution Explorer-> Project Name-> Resources-> Layout. Right-click to add a new item, select Layout, and give it a name, such as ItemLayout.axml. Open this layout file and add the following code.
 
(Folder Name: Layout , File Name: ItemLayout.axml)
  1. <?xml version="1.0" encoding="utf-8"?>    
  2. <android.support.v7.widget.CardView     
  3.     xmlns:android="http://schemas.android.com/apk/res/android"    
  4.     xmlns:card_view="http://schemas.android.com/apk/res-auto"    
  5.     android:layout_width="match_parent"    
  6.     android:layout_height="200dp"    
  7.     android:layout_margin="6dp"    
  8.     card_view:cardUseCompatPadding="true"    
  9.     card_view:cardElevation="4dp"    
  10.     >    
  11.     <ImageView     
  12.     android:layout_width="match_parent"    
  13.     android:layout_height="match_parent"    
  14.     android:id="@+id/imageitem"/>    
  15. </android.support.v7.widget.CardView>     
Step 5 - Add File Detail and Image Holder Class
 
Add two new classes with name FileDetails.cs and ImageHolder.cs by right-clicking on project, Add New Class, and replace the following code:
  1. public class FileInfo  
  2. {  
  3.     public string Name { getset; }  
  4.     public string Hash { getset; }  
  5. }  
  1. public class ImageHolder  
  2. {  
  3.     public string ImagePath { getset; }  
  4. }  
Step 6 - Writing the Adapter Class
 
After completing the layout, let’s start writing the adapter class to render the data. The RecyclerView adapter is the same as ListView but the override methods are different. Select Add -> New Item-> Class. Give it a name like CustomAdapter.cs and write the following code with appropriate namespaces.
 
(File Name: CustomAdapter.cs)
  1. public class CustomAdapter : Android.Support.V7.Widget.RecyclerView.Adapter  
  2. {  
  3.     private List<ImageHolder> mValues  = new List<ImageHolder>();  
  4.     public CustomAdapter(List<ImageHolder> list)  
  5.     {  
  6.         mValues = list;  
  7.     }  
  8.     public override int ItemCount => mValues.Count;  
  9.   
  10.     public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)  
  11.     {  
  12.   
  13.         ViewHolderTicket nView = holder as ViewHolderTicket;  
  14.         nView.mItem = mValues[position];  
  15.   
  16.         File imgFile = new File(mValues[position].ImagePath);  
  17.         if (imgFile.Exists())  
  18.         {  
  19.             Bitmap myBitmap = BitmapFactory.DecodeFile(imgFile.AbsolutePath);  
  20.   
  21.             nView._imageView.SetImageBitmap(myBitmap);  
  22.         }  
  23.     }  
  24.   
  25.     public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)  
  26.     {  
  27.         View view = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.ItemLayout, parent, false);  
  28.         ImageView imageView = view.FindViewById<ImageView>(Resource.Id.imageitem);  
  29.   
  30.         ViewHolderTicket vh = new ViewHolderTicket(view)  
  31.         {  
  32.             _imageView = imageView,                 
  33.         };  
  34.         return vh;  
  35.     }  
  36.     public void UpdateImages()  
  37.     {  
  38.         mValues.Clear();  
  39.     }  
  40. }  
  41. public class ViewHolderTicket : RecyclerView.ViewHolder  
  42.     {  
  43.         public ImageView _imageView { getset; }  
  44.         public ImageHolder mItem;  
  45.         public ViewHolderTicket(View view) : base(view)  
  46.         {  
  47.             _imageView = view.FindViewById<ImageView>(Resource.Id.imageitem);  
  48.         }  
  49.     }    
Step 7 - Main Activity
 
Lastly, go to Solution Explorer-> Project Name-> MainActivity and add the following code to the main activity with the appropriate namespaces.
 
(FileName: MainActivity)
  1. public class MainActivity : AppCompatActivity  
  2.     {  
  3.         List<ImageHolder> ToDelete;  
  4.         CustomAdapter _adapter;  
  5.         private Button btnChoose, btnDelete;  
  6.         private RecyclerView recyclerView;  
  7.         protected override void OnCreate(Bundle savedInstanceState)  
  8.         {  
  9.             base.OnCreate(savedInstanceState);  
  10.   
  11.             // Set our view from the "main" layout resource  
  12.             SetContentView(Resource.Layout.activity_main);  
  13.             recyclerView = FindViewById<RecyclerView>(Resource.Id.reycyclerViewImages);  
  14.             btnChoose = FindViewById<Button>(Resource.Id.btnChoose);  
  15.             btnDelete = FindViewById<Button>(Resource.Id.btnDelete);  
  16.   
  17.             //Events    
  18.             btnChoose.Click += delegate  
  19.             {  
  20.                double totalSize = 0;  
  21.                var fileLists =  AllImage();  
  22.                //int totalFiles = fileLists.Count;  
  23.                 List<FileInfo> finalInfo = new List<FileInfo>();  
  24.   
  25.                 ToDelete = new List<ImageHolder>();  
  26.   
  27.                     foreach (var item in fileLists)  
  28.                     {  
  29.                         using (var fs = new FileStream(item, FileMode.Open, FileAccess.Read))  
  30.                         {  
  31.                             finalInfo.Add(new FileInfo()  
  32.                             {  
  33.                                 Name = item,  
  34.                                 Hash = BitConverter.ToString(SHA1.Create().ComputeHash(fs)),  
  35.                             });  
  36.                         }  
  37.                     }  
  38.                   
  39.                 //group by file hash code  
  40.                 var similarList = finalInfo.GroupBy(f => f.Hash)  
  41.                     .Select(g => new { Hash = g.Key, Files = g.Select(z => z.Name).ToList() });  
  42.                 var somevar = similarList.SelectMany(f => f.Files.Skip(1)).ToList();  
  43.                 List<ImageHolder> secondList = new List<ImageHolder>();  
  44.                 foreach (var item in somevar)  
  45.                 {  
  46.                     secondList.Add(new ImageHolder { ImagePath = item });  
  47.                 }    
  48.                 ToDelete.AddRange(secondList);  
  49.                 var layoutManager = new LinearLayoutManager(this);  
  50.                 recyclerView.SetLayoutManager(layoutManager);  
  51.                 _adapter = new CustomAdapter(ToDelete);  
  52.                 recyclerView.SetAdapter(_adapter);  

  53.                 if (ToDelete.Count > 0)  
  54.                 {  
  55.                     foreach (var item in ToDelete)  
  56.                     {  
  57.                         FileInfo fi = new FileInfo(item.ImagePath);  
  58.                         totalSize += fi.Length;  
  59.                     }  
  60.                     btnDelete.Text = "Delete Size " + Math.Round((totalSize / 1000000), 2).ToString() + " MB";  
  61.                 }  
  62.                 Toast.MakeText(this"Total No of Duplicate Images " + ToDelete.Count, ToastLength.Long).Show();  
  63.             };  
  64.   
  65.             btnDelete.Click += delegate  
  66.             {  
  67.                 if(ToDelete.Count > 0)  
  68.                 {  
  69.                     foreach (var file in ToDelete)  
  70.                     {  
  71.                         var imgFile = new Java.IO.File(file.ImagePath);  
  72.                         if (imgFile.Exists())  
  73.                         {  
  74.                             imgFile.Delete();  
  75.                         }  
  76.                     }  
  77.                     _adapter.UpdateImages();  
  78.                 }  
  79.                 else  
  80.                 {  
  81.                     Toast.MakeText(this"Images not found for deleting.", ToastLength.Short).Show();  
  82.                 }   
  83.             };  
  84.         }  
  85.   
  86.         private List<string> AllImage()  
  87.         {  
  88.             List<string> AllImages = new List<string>();  
  89.               
  90.             Android.Net.Uri uri;  
  91.             ICursor cursor;  
  92.             int column_index_data;  
  93.             string absolutePathOfImage = null;  
  94.             uri = Android.Provider.MediaStore.Images.Media.ExternalContentUri;  
  95.             string[] projection = { MediaStore.MediaColumns.Data, MediaStore.Images.Media.InterfaceConsts.BucketDisplayName };  
  96.             cursor = ApplicationContext.ContentResolver.Query(uri, projection, nullnull,null);  
  97.             column_index_data = cursor.GetColumnIndexOrThrow(MediaStore.MediaColumns.Data);  
  98.             while (cursor.MoveToNext())  
  99.             {  
  100.                 absolutePathOfImage = cursor.GetString(column_index_data);  
  101.                 AllImages.Add(absolutePathOfImage);  
  102.             }  
  103.             //Toast.MakeText(this, "Number of Images " + AllImages.Count, ToastLength.Short).Show();  
  104.             return AllImages;  
  105.         }  
  106.     }  
Step 9 - Permissions From Device
 
We need permission from the device because we shall be using the device’s READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE. Please add External Storage permission to your AndroidManifest.xml.Let's open Solution Explorer-> Properties-> AndroidManifest and let's add the code inside application tags.
 
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 

Summary

 
This was the process of creating a Duplicate Image Detector app in Xamarin.Android, using Visual Studio. Please share your comments and feedback.