Tuesday, October 15, 2013

File Locker for Android

In this tutorial, you will learn to create a File Locker app for Android devices. The File Locker app displays a list of files that the user can select to lock or unlock. The directories also list so that the user is able to navigate from one directory to another. To lock or unlock a file, the user has to select the file and enter the password in to the password text box. Then the user will push the Lock button to lock the file or push the Unlock button to unlock the file.




When the file is locked, the locked icon will display next to the file (on the left) when its parent directory is refreshed. The file that is already locked is not allowed to lock again unless it is unlocked. To unlock the file the user must provide the correct password. It is the password that he/she uses to lock the file.

The user can choose to lock any file. The locked file will be not understandable since its content is encrypted. The encryption and decryption processes are performed on the file.The performance (speed of the processes) of the app still be maintained although the file is very large (e.g audio, video, and image files) . I will talk about this technique later in this post.

Now open your Eclipse and create a new project. The project name will be FileLocker. On the main interface, an EditText component is needed to allow the user to input the password. We need two Buttons. One is for locking the file and another one is for unlocking the file. We also need a ListView to display the list of files and directories. These components are defined in the activity_main.xml file that is the resource file of the MainActivity class. Here is its content.

activity_main.xml file

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:background="@drawable/back_style"
     >

  <EditText
        android:id="@+id/txt_input"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:inputType="textPassword"    
        android:hint="@string/txt_hint"
        />
        <LinearLayout
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:orientation="horizontal"
             >
<Button
          android:id="@+id/bt_lock"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@string/label_lock"
          android:onClick="lockFile"
          />
  <Button
          android:id="@+id/bt_unlock"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@string/label_unlock"
          android:onClick="unlockFile"
          />
  </LinearLayout>
   <TextView
    android:id="@+id/txt_view"
          android:layout_width="fill_parent"
          android:layout_height="wrap_content"
      />
        <ListView
            android:id="@+id/files_list"
            android:layout_width="fill_parent"
            android:layout_height="300dp"
            android:paddingBottom="5dp"
            android:paddingTop="5dp"
     
            />    

</LinearLayout>


The file that applies background style to the interface is called back_style.xml. It is saved in the drawable directory of the project. In this directory, you also need some icon images that are used in the File Locker app. You can download the content of the drawable directory from here.

back_style.xml file

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 
    <item>
        <shape>
            <gradient
                android:startColor="#dfdfdf"
                android:endColor="#dfdfdf"            
                android:angle="180" />
        </shape>      
    </item>
</selector>

The string values that are used in the activity_main.xml file are defined in the strings.xml file. This is the content of the strings.xml file.

strings.xml file

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">FileLocker</string>
    <string name="action_settings">Settings</string>
   <string name="label_lock">Lock</string>
   <string name="txt_hint">Enter password</string>
<string name="label_unlock">Unlock</string>
<string name="icon_image">Icon</string>

</resources>



The ListView has its own layout file. This file defines the template for an item of the list. An item of the list contains two parts. One part is the icon. It can be file icon, directory icon, or locked icon. Another part is the file or directory name. This layout file is called listlayout.xml file stored in the layout directory.

listlayout.xml file

<?xml version="1.0" encoding="utf-8"?>
<!--  Single List Item Design -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="5dip" >

<ImageView
    android:id="@+id/icon"
    android:layout_width="30dp"
    android:layout_height="30dp"
    android:padding="5sp"
    android:contentDescription="icon_image"
 />

<TextView
    android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10sp"
        android:textSize="20sp"
        android:textColor="#0000ff"
        android:textStyle="bold" >
</TextView>
</LinearLayout>


In default the ListView component displays only text. To enable the ListView to display both images and text, you need to customize it. The ListView customization starts from defining its layout file to include two components--image and text as you see in the listlayout.xml file shown above. Another step is to extend the ArrayAdapter class so that image and text can be placed on the components. The ListAdapterModel is created to accomplish this task. The ListAdapterModel object will contain data required to display on the list.

ListAdapterModel.java file

package com.example.filelocker;

import java.io.File;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;


public class ListAdapterModel extends ArrayAdapter<String>{
int groupid;
String[] names;
Context context;
String path;
public ListAdapterModel(Context context, int vg, int id, String[] names, String parentPath){
super(context,vg, id, names);
this.context=context;
groupid=vg;
this.names=names;
this.path=parentPath;
}
public View getView(int position, View convertView, ViewGroup parent) {

        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View itemView = inflater.inflate(groupid, parent, false);
        ImageView imageView = (ImageView) itemView.findViewById(R.id.icon);
        TextView textView = (TextView) itemView.findViewById(R.id.label);
        String item=names[position];
        textView.setText(item);
        File lockedfile=new File(context.getFilesDir(),item);
        if(lockedfile.exists()){
        //set the locked icon to the file that was already locked.
        imageView.setImageDrawable(context.getResources().getDrawable(R.drawable.locked_icon));
        }
        else{//set the directory and file icon to the unlocked file
        File f=new File(path+"/"+item);
        if(f.isDirectory())
        imageView.setImageDrawable(context.getResources().getDrawable(R.drawable.diricon));
        else
        imageView.setImageDrawable(context.getResources().getDrawable(R.drawable.fileicon));
        }
        return itemView;
}

}


To help us in locking and unlocking file processes, we will need a Locker class. The Locker.java file defines a class called Locker. This class contains methods that will be used in locking and unlocking file process.

Locker.java file

package com.example.filelocker;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import android.content.Context;
import android.webkit.MimeTypeMap;

class Locker{
String path;
String pwd;
Context context;
final String separator="--*****--";

Locker(Context context,String path,String pwd){
this.path=path;
this.pwd=pwd;
this.context =context;
}


public boolean isTextFile(String file){

boolean isText=false;
String extension = MimeTypeMap.getFileExtensionFromUrl(file);
   String mimeType=MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
   if(mimeType.startsWith("text/"))
    isText=true;
        return isText;
}

public void lock(){
boolean isHead=true;
boolean isBody=false;
int blockSize=0;

try{
File f=new File(path);
//get previous pwd
if(f.exists()){
byte[] ppwd=getPwd();
if(ppwd!=null){
MessageAlert.showAlert("Alreadly locked",context);
return;
}
FileInputStream fis=new FileInputStream(f);
File tempfile=new File(context.getFilesDir(),"temp.temp");
FileOutputStream fos=new FileOutputStream(tempfile);
FileChannel fc=fis.getChannel();
int pwdInt=bytearrayToInt(pwd.getBytes());
int nRead;
boolean isText=isTextFile(path);
if(isText){ //encrypting two parts of the text file
blockSize=(int)f.length()/4; //25 percent of the file content
ByteBuffer bb=ByteBuffer.allocate(blockSize);

while ( (nRead=fc.read( bb )) != -1 )
{
bb.position(0);
bb.limit(nRead);

//encrypt the head section of the file
if(isHead){
while ( bb.hasRemaining())
fos.write(bb.get()+pwdInt);
isHead=false;
isBody=true;
}
else if(isBody){
//do not decrypt the body section of the file
fos.write(bb.array());
isBody=false;
}
else{//encrypt the footer section of the file
while ( bb.hasRemaining())
fos.write(bb.get()+pwdInt);
}

bb.clear();

}
}

else{
blockSize=1024; //encrypt the first 1kb of the file for non-text file
ByteBuffer bb=ByteBuffer.allocate(blockSize);

while ( (nRead=fc.read( bb )) != -1 )
{
bb.position(0);
bb.limit(nRead);
//encrypt only the head section of the file
if(isHead){
while ( bb.hasRemaining())
fos.write(bb.get()+pwdInt);
isHead=false;

}
else{
fos.write(bb.array());
}
bb.clear();

}
}

fis.close();
fos.flush();
fos.close();
//replacing the file content
f.delete();
File lockedFile=new File(path);
copyFile(tempfile,lockedFile);
//delete the temp file
tempfile.delete();
//save the password
saveInfo(pwd,blockSize);
//make the file read only
lockedFile.setReadOnly();

}

}catch(IOException e){e.printStackTrace();}
}


public void unlock(){
boolean isHead=true;
boolean isBody=false;
int pwdread=bytearrayToInt(getPwd());
int pwdbyte=bytearrayToInt(pwd.getBytes());
if(pwdbyte==pwdread){

try{
File f=new File(path);
if(f.exists()){

FileInputStream fis=new FileInputStream(f);
File tempfile=new File(context.getFilesDir(),"temp.temp");
FileOutputStream fos=new FileOutputStream(tempfile);
FileChannel fc=fis.getChannel();
int pwdInt=bytearrayToInt(pwd.getBytes());
int blockSize=getBlockSize();
ByteBuffer bb=ByteBuffer.allocate(blockSize);
int nRead;
boolean isText=isTextFile(path);
if(isText){ //decoding two parts of the text file
while ( (nRead=fc.read( bb )) != -1 )
{
bb.position(0);
bb.limit(nRead);

//decrypt the head section of the file
if(isHead){
while ( bb.hasRemaining())
fos.write(bb.get()-pwdInt);
isHead=false;
isBody=true;
}
else if(isBody){
//do not decrypt the body section of the file
fos.write(bb.array());
isBody=false;
}
else{//decrypt the footer section of the file
while ( bb.hasRemaining())
fos.write(bb.get()-pwdInt);
}

bb.clear();

}
}

else{

while ( (nRead=fc.read( bb )) != -1 )
{
bb.position(0);
bb.limit(nRead);
//encrypting only the head section of the file
if(isHead){
while ( bb.hasRemaining())
fos.write(bb.get()-pwdInt);
isHead=false;

}
else{
fos.write(bb.array());
}
bb.clear();

}
}

fis.close();
fos.flush();
fos.close();
//Replacing the file content
f.delete();
File unlockedFile=new File(path);
unlockedFile.setWritable(true);
copyFile(tempfile,unlockedFile);
//delete the temp file
tempfile.delete();
File filepwd=new File(context.getFilesDir(),getName(path));
//delete the password
filepwd.delete();



}

}catch(IOException e){e.printStackTrace();}
}
else{
MessageAlert.showAlert("Invalid password or file is not locked.",context);
}
}

private void copyFile(File src, File dst) throws IOException
{
FileInputStream fis=new FileInputStream(src);
FileOutputStream fos=new FileOutputStream(dst);
    FileChannel inChannel =fis.getChannel();
    FileChannel outChannel = fos.getChannel();
    try
    {
        inChannel.transferTo(0, inChannel.size(), outChannel);
    }catch(IOException e){e.printStackTrace();}
    finally
    {
        if (inChannel != null){
        fis.close();
            inChannel.close();
        }
        if (outChannel != null){
        fos.close();
            outChannel.close();
        }
       
    }
}
private int bytearrayToInt(byte[] pwd){
int b=0;
if(pwd!=null)
for(byte y:pwd){
b=b+y;
}
return b;

}

private byte[] getPwd(){
byte[] b=null;
try {
File f=new File(context.getFilesDir(),getName(path));
if(f.exists()){
BufferedReader br=new BufferedReader(new FileReader(f));
String info=br.readLine();
b=info.substring(0,info.lastIndexOf(separator)).getBytes();
br.close();
}
} catch(Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

return b;

}

private int getBlockSize(){
int size=0;
try {
File f=new File(context.getFilesDir(),getName(path));
if(f.exists()){
BufferedReader br=new BufferedReader(new FileReader(f));
String info=br.readLine();
size=Integer.valueOf(info.substring(info.lastIndexOf(separator)+separator.length()));
br.close();
}
} catch(Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

return size;

}

private void saveInfo(String pwd,int blockSize){
try {
String fileName=getName(path);
File f=new File(context.getFilesDir(),fileName);
BufferedWriter bw=new BufferedWriter(new FileWriter(f));
String info=pwd+separator+blockSize;
bw.write(info);
bw.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

private String getName(String path){
return(path.substring(path.lastIndexOf("/")+1));
}



 
}


The constructor of the Locker class will receive the selected file path, password, and context sent from the MainActivity class. These information are used in the locking and unlocking file processes.
The isTextFile method will be called to check whether the selected file is a text file. To improve the performance of locking and unlocking processes, text file and other types files will be encrypted differently .

The lock method locks the selected file by encrypting it so that it is not understandable. If the file is a text file, the two parts (header and footer) of the file will be encrypted. The header part will be 25 percent of file content. The body part is also 25 percent of the file content. The rest is the footer part. The body part will not be encrypted. You can encrypt all content of the file. However, its performance will degrade when the file is large. Another thing that we try to do in improving the locking or unlocking process' performance is using the FileChannel with ByteBuffer to read block of bytes from the source file and write this block to the temporary file. The content of the temporary file will replace the content of the source file. Reading a block of file that contains many bytes at a time and reading one byte at a time are different. Reading each block of the file until all blocks are read is faster than reading one byte one until all content of the file is read. In this locking or unlocking text file process, each block that occupies 25 percent of the file content will be read a a time.

Header (25% or 1/4)
Body (25% or 1/4)
Footer (50% or 1/2)

Encrypting the file content is simple. In this FileLocker app, we encrypt the file content by modifying the byte data of the file. For the part of content to be encrypted, its every byte is added to the sum of bytes generated from the password text.

fos.write(bb.get()+pwdInt);

If the selected file is not a text file, the locking or unlocking process is performed only at the header section of the file. So large audio, video, or image files can be locked or unlocked very fast. The size of header block is 1024 bytes. Each block of the file of this size will be read from the source file that is not the text file.

The unlock method will be called to unlock the locked file by decrypting parts of the file that were encrypted in the locking process. In the unlocking process every byte of each of the file content is subtracted by the sum of bytes previously added to every byte.

fos.write(bb.get()-pwdInt);

The copyFile method is invoked by the lock and unlock method to copy the content of the temparary file to the destination file in file content replacement process.

The bytearrayToInt sums all bytes in the input array of bytes. This method is called to sum all bytes generated from the password text.

The getPwd method converts the password text in to an array of bytes.

The getBlockSize method is invoked by the unlocking process to read the size of the encrypted block from the file that is saved in the locking process. So the unlocking process knows the block of bytes to be read and decrypted. This file stores the password text and the encrypted block size.

The saveInfo method is invoked by the locking process to save the password text and the block size to a file. The password text and the block size is stored in the file in the form as shown below.

password--*****--size

The last method of the Locker class is getName. This method simply returns the file name of the selected file path.

Now we take a look at the MainActivity class that is in the MainActivity.java file.
MainActivity.java file

package com.example.filelocker;
import java.io.File;

import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText;

import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity {


   private String path="";
   private String selectedFile="";
   private Context context;
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.context=this;
 
    }

    protected void onStart(){
    super.onStart();
    ListView lv=(ListView) findViewById(R.id.files_list);
if(lv!=null){
lv.setSelector(R.drawable.selection_style);
lv.setOnItemClickListener(new ClickListener());
}
path="/mnt";
listDirContents();
    }
 
    public void onBackPressed(){
    if(path.length()>1){ //up one level of directory structure
    File f=new File(path);
    path=f.getParent();
    listDirContents();
    }
    else{
    refreshThumbnails();
    System.exit(0); //exit app
   
    }
    }
 
 
    private void refreshThumbnails(){
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory())));
}
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

 
    private class ClickListener implements OnItemClickListener{
      public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
      //selected item      
            ViewGroup vg=(ViewGroup)view;
      String selectedItem = ((TextView) vg.findViewById(R.id.label)).getText().toString();
            path=path+"/"+selectedItem;
            //et.setText(path);          
            listDirContents();
      }
     
     
      }
 
 
 
    private void listDirContents(){
    ListView l=(ListView) findViewById(R.id.files_list);
    if(path!=null){
    try{
    File f=new File(path);
    if(f!=null){
    if(f.isDirectory()){
    String[] contents=f.list();
    if(contents.length>0){
    //create the data source for the list
    ListAdapterModel lm=new ListAdapterModel(this,R.layout.listlayout,R.id.label,contents,path);
    //supply the data source to the list so that they are ready to display
    l.setAdapter(lm);
    }
    else
    {
    //keep track the parent directory of empty directory
    path=f.getParent();
    }
    }
    else{
    //capture the selected file path
    selectedFile=path;
    //keep track the parent directory of the selected file
    path=f.getParent();
   
    }
    }
    }catch(Exception e){}
    }    
 
   
    }
 
    public void lockFile(View view){
    EditText txtpwd=(EditText)findViewById(R.id.txt_input);
String pwd=txtpwd.getText().toString();
if(pwd.length()>0){

if(selectedFile.length()>0){
BackTaskLock btlock=new BackTaskLock();
btlock.execute(pwd,null,null);

}
else{
MessageAlert.showAlert("Please a select a file to lock",context);
}
}
else{
MessageAlert.showAlert("Please enter password",context);
}
    }
 
    public void startLock(String pwd){
    Locker locker=new Locker(context,selectedFile,pwd);
locker.lock();
    }

    public void unlockFile(View view){
    EditText txtpwd=(EditText)findViewById(R.id.txt_input);
String pwd=txtpwd.getText().toString();
if(pwd.length()>0){

if(selectedFile.length()>0){

BackTaskUnlock btunlock=new BackTaskUnlock();
btunlock.execute(pwd,null,null);
   
}
else{
MessageAlert.showAlert("Please select a file to unlock",context);
}
}
else{
MessageAlert.showAlert("Please enter password",context);
}

    }
 
    public void startUnlock(String pwd){
    Locker locker=new Locker(context,selectedFile,pwd);
locker.unlock();
    }
 
    private class BackTaskLock extends AsyncTask<String,Void,Void>{
    ProgressDialog pd;
    protected void onPreExecute(){
super.onPreExecute();
//show process dialog
pd = new ProgressDialog(context);
pd.setTitle("Locking the file");
pd.setMessage("Please wait.");
pd.setCancelable(true);
pd.setIndeterminate(true);
pd.show();


}
protected Void doInBackground(String...params){    
try{

startLock(params[0]);

}catch(Exception e){
pd.dismiss();   //close the dialog if error occurs
}
return null;

}
protected void onPostExecute(Void result){
pd.dismiss();
}


}
 
    private class BackTaskUnlock extends AsyncTask<String,Void,Void>{
    ProgressDialog pd;
    protected void onPreExecute(){
super.onPreExecute();
//show process dialog
pd = new ProgressDialog(context);
pd.setTitle("UnLocking the file");
pd.setMessage("Please wait.");
pd.setCancelable(true);
pd.setIndeterminate(true);
pd.show();


}
protected Void doInBackground(String...params){    
try{

startUnlock(params[0]);

}catch(Exception e){
pd.dismiss();   //close the dialog if error occurs
}
return null;

}
protected void onPostExecute(Void result){
pd.dismiss();
}


}

 
}


In the onStart method of MainActivity class, the ListView component is registered to the item click event. Immediately, the listDirContents method is invoked to display files and directories in the /mnt directory. The selection style of the list is defined by the selection_style.xml file.

selection_style.xml file.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
 
    <item>
        <shape>
            <gradient
                android:startColor="#dfdfdf"
                android:endColor="#dfdfdf"            
                android:angle="180" />
        </shape>      
    </item>
</selector>

The onBackPressed method is invoked every time the user presses the Back button on the  device. It will move back one level of the directory structure until the root directory is reached.

The refreshThumbnails method is invoked in case that the root directory is reached when the user presses the Back button. The external card will be refreshed at that time. This is to make sure that any change that is made to the media files ( audio, audio, or video files) will change the thumbnails of the files.

The lockFile method will be invoked when the user pushes the Lock button. The locking process might take long time. So it is placed in the background by using the AsyncTask class. In the doInBackground method of the AsyncTask class, the lock method of the Locker class is called from the startLock method to perform the locking process. The progress dialog displays to inform the user to wait until the locking process completes.



The unlockFile method is invoked when the user pushes the Unlock button. This process is also placed in background by using the AsyncTask. This method will subsequently invokes the unlock method of the Locker class to unlock the file. The progress dialog displays to inform the user to wait until the unlocking process completes.



When the user touches the Lock or Unlock button without entering the password or selecting the file, the alert dialog displays to inform the user about this action. Since the code to display the dialog is used in different classes (MainActivity and Locker class ), it is defined in a separate class called MessageAlert. Below is the content of the MessageAlert class.

MessageAlert.java file

package com.example.filelocker;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;

public class MessageAlert {
//This method will be invoked to display alert dialog
    public static void showAlert(String message,Context context){
   
    AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setMessage(message);
        builder.setCancelable(true);
        builder.setPositiveButton("OK", new OnClickListener(){
        public void onClick(DialogInterface dialog, int which) {
          dialog.dismiss();
          }

        });
        AlertDialog dialog = builder.create();      
        dialog.show();
   
    }
}


Before running the FileLocker app, you will need to allow it to use the external storage by adding the below code to the AndroidManifest.xml file.

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Download the apk file of the FileLocker app

No comments:

Post a Comment