Major Refactor!

Comprehensive refactoring of DeviceListFragment, includes:
- Shifting all async-tasks to separate files
- Exceptions in the async-tasks are now handled by showing a toast and closing the LocalFileTransferActivity
- Added a new 'Error' status for file items
- If a file has error status after transfer, then an error icon is shown for it TransferProgressFragment
- Documentation and other additions/changes
This commit is contained in:
Aditya-Sood 2019-06-29 18:18:58 +05:30
parent 147dbb033b
commit 91b26a9f08
9 changed files with 536 additions and 458 deletions

View File

@ -1,14 +1,11 @@
package org.kiwix.kiwixmobile.zim_manager.local_file_transfer;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.net.Uri;
import android.net.wifi.p2p.WifiP2pDevice;
import android.net.wifi.p2p.WifiP2pDeviceList;
import android.net.wifi.p2p.WifiP2pInfo;
import android.net.wifi.p2p.WifiP2pManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@ -23,7 +20,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.fragment.app.ListFragment;
@ -31,73 +27,115 @@ import androidx.fragment.app.ListFragment;
import org.kiwix.kiwixmobile.R;
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import static org.kiwix.kiwixmobile.zim_manager.local_file_transfer.FileItem.TO_BE_SENT;
/**
* Part of the local file sharing module, this fragment is responsible for displaying details of
* the user device and the list of available peer devices, and once the user taps on a particular
* peer device item, connecting to that device and initiating file transfer.
*
* File transfer involves two phases:
* 1) Handshake with the selected peer device, using {@link PeerGroupHandshakeAsyncTask}
* 2) After handshake, starting the files transfer using {@link SenderDeviceAsyncTask} on the sender
* device and {@link ReceiverDeviceAsyncTask} files receiving device
*
* The starting point for the module is {@link LocalFileTransferActivity}
* */
public class DeviceListFragment extends ListFragment implements WifiP2pManager.PeerListListener, WifiP2pManager.ConnectionInfoListener {
public static final String TAG = "DeviceListFragment";
private SharedPreferenceUtil sharedPreferenceUtil;
private View fragRootView = null;
private List<WifiP2pDevice> peerDevices = new ArrayList<WifiP2pDevice>();
private WifiP2pDevice userDevice;
private WifiP2pDevice selectedPeerDevice = null;
private InetAddress selectedPeerDeviceInetAddress;
private WifiP2pInfo groupInfo;
private InetAddress fileReceiverAddress;
public static int PEER_HANDSHAKE_PORT = 8009;
public static int FILE_TRANSFER_PORT = 8008;
private boolean fileSender = false;
private ArrayList<Uri> fileUriList;
private int totalFiles = -1;
private int totalFilesSent = 0;
private ArrayList<FileItem> fileItems = new ArrayList<>();
private TransferProgressFragment transferProgressFragment;
private SharedPreferenceUtil sharedPreferenceUtil;
private LocalFileTransferActivity localFileTransferActivity; // Parent activity, starting point of the module
private TransferProgressFragment transferProgressFragment; // Sibling fragment, for displaying transfer progress
private View deviceListFragmentRootView = null; // Root view corresponding to the DeviceListFragment
private boolean fileSender = false; // Whether file sending device or not
private ArrayList<Uri> fileUriList; // Uris of files to be shared, available only for the sender device
private int totalFilesForTransfer = -1;
private int filesSent = 0; // Count of number of files transferred until now
private ArrayList<FileItem> filesToSend = new ArrayList<>();
private WifiP2pDevice userDevice; // Represents the device on which the app is running
private WifiP2pInfo groupInfo; // Corresponds to the WiFi P2P group formed between the two devices
private List<WifiP2pDevice> peerDevices = new ArrayList<WifiP2pDevice>();
private WifiP2pDevice selectedPeerDevice = null;
private InetAddress selectedPeerDeviceInetAddress;
private InetAddress fileReceiverDeviceAddress; // IP address of the file receiving device
private boolean fileTransferStarted = false;
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
this.setListAdapter(new WifiPeerListAdapter(getActivity(), R.layout.row_peer_device, peerDevices));
localFileTransferActivity = (LocalFileTransferActivity) getActivity();
if(((LocalFileTransferActivity) getActivity()).isFileSender()) {
// As DeviceListFragment extends ListFragment for the purpose of displaying list of peers
this.setListAdapter(new WifiPeerListAdapter(localFileTransferActivity, R.layout.row_peer_device, peerDevices));
if(localFileTransferActivity.isFileSender()) {
fileSender = true;
fileUriList = ((LocalFileTransferActivity) getActivity()).getFileURIArrayList();
totalFiles = fileUriList.size();
fileUriList = localFileTransferActivity.getFileUriArrayList();
totalFilesForTransfer = fileUriList.size();
for(int i = 0; i < fileUriList.size(); i++)
fileItems.add(new FileItem(getFileName(fileUriList.get(i)), TO_BE_SENT));
for(int i = 0; i < fileUriList.size(); i++) {
filesToSend.add(new FileItem(getFileName(fileUriList.get(i)), FileItem.TO_BE_SENT));
}
displayTransferProgressFragment();
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
fragRootView = inflater.inflate(R.layout.fragment_device_list, null);
return fragRootView;
deviceListFragmentRootView = inflater.inflate(R.layout.fragment_device_list, null);
return deviceListFragmentRootView;
}
public WifiP2pDevice getUserDevice() { return userDevice; }
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
/* Connection can only be initiated by user of the sender device, & only when transfer has not been started */
if(!isFileSender() || fileTransferStarted)
return;
private static String getDeviceStatus(int status) {
selectedPeerDevice = (WifiP2pDevice) getListAdapter().getItem(position);
Toast.makeText(localFileTransferActivity, selectedPeerDevice.deviceName, Toast.LENGTH_SHORT).show();
((DeviceActionListener) localFileTransferActivity).connect(selectedPeerDevice);
}
private void displayTransferProgressFragment() {
transferProgressFragment = new TransferProgressFragment(filesToSend);
FragmentManager fragmentManager = localFileTransferActivity.getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.container_fragment_transfer_progress, transferProgressFragment)
.commit();
}
public void updateUserDevice(WifiP2pDevice device) {
this.userDevice = device;
if(userDevice != null) {
TextView deviceName = deviceListFragmentRootView.findViewById(R.id.text_view_device_name);
TextView deviceStatus = deviceListFragmentRootView.findViewById(R.id.text_view_device_status);
if(deviceName != null) deviceName.setText(userDevice.deviceName);
if(deviceStatus != null) deviceStatus.setText(getDeviceStatus(userDevice.status));
}
}
public static String getDeviceStatus(int status) {
Log.d(LocalFileTransferActivity.TAG, "Peer Status: " + status);
switch (status) {
@ -111,36 +149,12 @@ public class DeviceListFragment extends ListFragment implements WifiP2pManager.P
}
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
if(!isFileSender())
return;
selectedPeerDevice = (WifiP2pDevice) getListAdapter().getItem(position);
Toast.makeText(getActivity(), selectedPeerDevice.deviceName, Toast.LENGTH_SHORT).show();
// TODO: Set sender depending upon receiving file URI in activity opening intent - DONE
((DeviceActionListener) getActivity()).connect(selectedPeerDevice);
}
public void updateUserDevice(WifiP2pDevice device) {
this.userDevice = device;
if(userDevice != null) {
TextView deviceName = fragRootView.findViewById(R.id.text_view_device_name);
TextView deviceStatus = fragRootView.findViewById(R.id.text_view_device_status);
if(deviceName != null) deviceName.setText(userDevice.deviceName);
if(deviceStatus != null) deviceStatus.setText(getDeviceStatus(userDevice.status));
}
}
@Override
public void onPeersAvailable(WifiP2pDeviceList peers) {
ProgressBar searchingPeersProgressBar = fragRootView.findViewById(R.id.progress_bar_searching_peers);
ProgressBar searchingPeersProgressBar = deviceListFragmentRootView.findViewById(R.id.progress_bar_searching_peers);
searchingPeersProgressBar.setVisibility(View.GONE);
FrameLayout frameLayoutPeerDevices = fragRootView.findViewById(R.id.frame_layout_peer_devices);
FrameLayout frameLayoutPeerDevices = deviceListFragmentRootView.findViewById(R.id.frame_layout_peer_devices);
frameLayoutPeerDevices.setVisibility(View.VISIBLE);
peerDevices.clear();
@ -158,9 +172,9 @@ public class DeviceListFragment extends ListFragment implements WifiP2pManager.P
}
public void onInitiateDiscovery() {
ProgressBar searchingPeersProgressBar = fragRootView.findViewById(R.id.progress_bar_searching_peers);
ProgressBar searchingPeersProgressBar = deviceListFragmentRootView.findViewById(R.id.progress_bar_searching_peers);
searchingPeersProgressBar.setVisibility(View.VISIBLE);
FrameLayout frameLayoutPeerDevices = fragRootView.findViewById(R.id.frame_layout_peer_devices);
FrameLayout frameLayoutPeerDevices = deviceListFragmentRootView.findViewById(R.id.frame_layout_peer_devices);
frameLayoutPeerDevices.setVisibility(View.INVISIBLE);
}
@ -168,54 +182,87 @@ public class DeviceListFragment extends ListFragment implements WifiP2pManager.P
public void onConnectionInfoAvailable(WifiP2pInfo info) {
groupInfo = info;
//TODO: Show progress of file transfer process - DONE
new PeerGroupHandshakeAsyncTask(this, groupInfo).execute();
// TODO: Disable onclick listener (of list) for connecting to devices
// TODO: Disable onclick listener (of list) for connecting to devices - DONE using boolean startedFileTransfer
}
public void setClientAddress(InetAddress clientAddress) {
if(clientAddress != null) selectedPeerDeviceInetAddress = clientAddress;
if(clientAddress == null) {
// null is returned only in case of a failed handshake
Toast.makeText(localFileTransferActivity, "Peer device not cooperating for transfer", Toast.LENGTH_LONG).show();
localFileTransferActivity.closeLocalFileTransferActivity();
return;
}
// If control reaches here, means handshake was successful
selectedPeerDeviceInetAddress = clientAddress;
startFileTransfer();
}
private void startFileTransfer() {
fileTransferStarted = true;
if(groupInfo.groupFormed && !fileSender) {
displayTransferProgressFragment();
new ReceiverDeviceAsyncTask(this, transferProgressFragment).execute();
Toast.makeText(localFileTransferActivity, "Preparing to receive files...", Toast.LENGTH_SHORT).show();
} else if(groupInfo.groupFormed) {
{
Log.d(LocalFileTransferActivity.TAG, "Starting file transfer");
Toast.makeText(localFileTransferActivity, "Starting file transfer...", Toast.LENGTH_SHORT).show();
if(groupInfo.isGroupOwner) fileReceiverDeviceAddress = selectedPeerDeviceInetAddress;
else fileReceiverDeviceAddress = groupInfo.groupOwnerAddress;
//TODO: Fix this
for(int i = 0; i < 20000000; i++);
for(int i = 0; i < totalFilesForTransfer; i++) {
new SenderDeviceAsyncTask(this, transferProgressFragment, i).execute(fileUriList.get(i));
}
}
}
}
/* Helper methods */
public WifiP2pDevice getUserDevice() {
return userDevice;
}
public boolean isFileSender() {
return fileSender;
}
public int getTotalFiles() {
return totalFiles;
public int getTotalFilesForTransfer() {
return totalFilesForTransfer;
}
public void setTotalFiles(int totalFiles) {
this.totalFiles = totalFiles;
public void setTotalFilesForTransfer(int totalFilesForTransfer) {
this.totalFilesForTransfer = totalFilesForTransfer;
}
public ArrayList<FileItem> getFileItems() {
return fileItems;
return filesToSend;
}
public void setFileItems(ArrayList<FileItem> fileItems) {
this.fileItems = fileItems;
this.filesToSend = fileItems;
}
public ArrayList<Uri> getFileUriList() {
return fileUriList;
}
public void incrementTotalFilesSent() {
this.totalFilesSent++;
}
public int getTotalFilesSent() {
return totalFilesSent;
this.filesSent++;
}
public boolean allFilesSent() {
return (totalFilesSent == totalFiles);
return (filesSent == totalFilesForTransfer);
}
public void setSharedPreferenceUtil(SharedPreferenceUtil sharedPreferenceUtil) {
@ -226,42 +273,32 @@ public class DeviceListFragment extends ListFragment implements WifiP2pManager.P
return (sharedPreferenceUtil.getPrefStorage() + "/Kiwix/");
}
private void displayTransferProgressFragment() {
transferProgressFragment = new TransferProgressFragment(fileItems);
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.container_fragment_transfer_progress, transferProgressFragment)
.commit();
public InetAddress getFileReceiverDeviceAddress() {
return fileReceiverDeviceAddress;
}
private void startFileTransfer() {
if(groupInfo.groupFormed && !fileSender) {
displayTransferProgressFragment();
public static String getFileName(Uri fileUri) {
String fileUriString = fileUri.toString();
// Returns text after location of last slash in the file path
return fileUriString.substring(fileUriString.lastIndexOf('/')+1);
}
new FileServerAsyncTask(getActivity(), this, transferProgressFragment).execute();
Toast.makeText(getActivity(), "File receiving device", Toast.LENGTH_SHORT).show();
public static void copyToOutputStream(InputStream inputStream, OutputStream outputStream) throws IOException {
byte[] bufferForBytes = new byte[1024];
int bytesRead;
} else if(groupInfo.groupFormed) {
{
Log.d(LocalFileTransferActivity.TAG, "Starting file transfer");
Toast.makeText(getActivity(), "Starting file transfer", Toast.LENGTH_SHORT).show();
if(groupInfo.isGroupOwner) fileReceiverAddress = selectedPeerDeviceInetAddress;
else fileReceiverAddress = groupInfo.groupOwnerAddress;
//TODO: Fix this
for(int i = 0; i < 10000000; i++);
for(int i = 0; i < totalFiles; i++) {
new FileSenderAsyncTask(getContext(), this, groupInfo, transferProgressFragment, i).execute(fileUriList.get(i));
transferProgressFragment.changeStatus(i, FileItem.SENDING);
}
}
Log.d(TAG, "Copying to OutputStream...");
while ((bytesRead = inputStream.read(bufferForBytes)) != -1) {
outputStream.write(bufferForBytes, 0, bytesRead);
}
outputStream.close();
inputStream.close();
Log.d(LocalFileTransferActivity.TAG, "Both streams closed");
}
private class WifiPeerListAdapter extends ArrayAdapter<WifiP2pDevice> {
private List<WifiP2pDevice> listItems;
@ -277,7 +314,7 @@ public class DeviceListFragment extends ListFragment implements WifiP2pManager.P
View rowView = convertView;
if(rowView == null) {
LayoutInflater layoutInflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LayoutInflater layoutInflater = getActivity().getLayoutInflater();
rowView = layoutInflater.inflate(R.layout.row_peer_device, parent, false);
}
@ -287,7 +324,7 @@ public class DeviceListFragment extends ListFragment implements WifiP2pManager.P
TextView deviceName = rowView.findViewById(R.id.row_device_name);
TextView deviceStatus = rowView.findViewById(R.id.row_device_status);
if(deviceName != null) deviceName.setText(device.deviceName);
if(deviceName != null) deviceName.setText(device.deviceName);
if(deviceStatus != null) deviceStatus.setText(getDeviceStatus(device.status));
}
@ -298,334 +335,8 @@ public class DeviceListFragment extends ListFragment implements WifiP2pManager.P
public interface DeviceActionListener {
void cancelSearch();
void connect(WifiP2pDevice peerDevice);
void connect(@NonNull WifiP2pDevice peerDevice);
void closeLocalFileTransferActivity();
}
public static class PeerGroupHandshakeAsyncTask extends AsyncTask<Void, Void, InetAddress> {
private DeviceListFragment deviceListFragment;
private WifiP2pInfo groupInfo;
public PeerGroupHandshakeAsyncTask(DeviceListFragment deviceListFragment, WifiP2pInfo groupInfo) {
this.deviceListFragment = deviceListFragment;
this.groupInfo = groupInfo;
}
@Override
protected InetAddress doInBackground(Void... voids) {
if(groupInfo.groupFormed && groupInfo.isGroupOwner) {
try {
ServerSocket serverSocket = new ServerSocket(PEER_HANDSHAKE_PORT);
serverSocket.setReuseAddress(true);
Socket client = serverSocket.accept();
ObjectInputStream objectInputStream = new ObjectInputStream(client.getInputStream());
Object object = objectInputStream.readObject();
if (object.getClass().equals(String.class) && ((String) object).equals("Request Kiwix File Sharing")) {
Log.d(TAG, "Client IP address: "+ client.getInetAddress());
if(deviceListFragment.isFileSender()) {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(client.getOutputStream());
objectOutputStream.writeObject(""+deviceListFragment.getTotalFiles());
Log.d(TAG, "### SENDER DEVICE");
ArrayList<Uri> fileUriList = deviceListFragment.getFileUriList();
for (int i = 0; i < fileUriList.size(); i++) {
objectOutputStream.writeObject(getFileName(fileUriList.get(i)));
}
} else {
Object totalFilesObject = objectInputStream.readObject();
if(totalFilesObject.getClass().equals(String.class)) {
int total = Integer.parseInt((String) totalFilesObject);
deviceListFragment.setTotalFiles(total);
Log.d(TAG, "### Transfer of "+ total + " files");
ArrayList<FileItem> fileItems = new ArrayList<>();
for (int i = 0; i < total; i++) {
Object fileNameObject = objectInputStream.readObject();
if(fileNameObject.getClass().equals(String.class)){
Log.d(TAG, "File - "+ (String) fileNameObject);
fileItems.add(new FileItem((String) fileNameObject, TO_BE_SENT));
}
}
deviceListFragment.setFileItems(fileItems);
}
}
}
return client.getInetAddress();
} catch (Exception e) {
e.printStackTrace();
return null;
}
} else if(groupInfo.groupFormed && !groupInfo.isGroupOwner) {
String hostAddress = groupInfo.groupOwnerAddress.getHostAddress();
try {
Socket socket = new Socket();
socket.setReuseAddress(true);
socket.connect((new InetSocketAddress(hostAddress, PEER_HANDSHAKE_PORT)), 15000);
OutputStream os = socket.getOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(os);
oos.writeObject(new String("Request Kiwix File Sharing"));
if(deviceListFragment.isFileSender()) {
oos.writeObject(""+deviceListFragment.getTotalFiles());
Log.d(TAG, "### SENDER DEVICE");
ArrayList<Uri> fileUriList = deviceListFragment.getFileUriList();
for (int i = 0; i < fileUriList.size(); i++) {
oos.writeObject(getFileName(fileUriList.get(i)));
}
} else {
ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
Object totalFilesObject = objectInputStream.readObject();
if(totalFilesObject.getClass().equals(String.class)) {
int total = Integer.parseInt((String) totalFilesObject);
deviceListFragment.setTotalFiles(total);
Log.d(TAG, "### Transfer of "+ total + " files");
ArrayList<FileItem> fileItems = new ArrayList<>();
for (int i = 0; i < total; i++) {
Object fileNameObject = objectInputStream.readObject();
if(fileNameObject.getClass().equals(String.class)) {
Log.d(TAG, "File - "+ (String) fileNameObject);
fileItems.add(new FileItem((String) fileNameObject, TO_BE_SENT));
}
}
deviceListFragment.setFileItems(fileItems);
}
}
oos.close();
os.close();
socket.close();
return null;
} catch (Exception e) {
//Log.d(TAG, e.getMessage());
e.printStackTrace();
return null;
}
}
return null;
}
@Override
protected void onPostExecute(InetAddress inetAddress) {
(deviceListFragment).setClientAddress(inetAddress);
}
}
public static String getFileName(Uri fileUri) {
String fileName = "";
String fileUriString = fileUri.toString();
// Searches for location of last slash in the file path
for(int loc = fileUriString.length()-1; loc >= 0; loc--) {
if(fileUriString.charAt(loc) == '/') {
return fileUriString.substring(loc+1);
}
}
return null;
}
public InetAddress getFileReceiverAddress() {
return fileReceiverAddress;
}
public static class FileSenderAsyncTask extends AsyncTask<Uri, Void, String> {
private Context context;
private DeviceListFragment deviceListFragment;
private WifiP2pInfo groupInfo;
private TransferProgressFragment transferProgressFragment;
private ArrayList<FileItem> fileItems;
private int fileItemIndex;
public FileSenderAsyncTask(Context context, DeviceListFragment deviceListFragment, WifiP2pInfo groupInfo, TransferProgressFragment transferProgressFragment, int fileItemIndex) {
this.context = context;
this.deviceListFragment = deviceListFragment;
this.groupInfo = groupInfo;
this.transferProgressFragment = transferProgressFragment;
this.fileItems = deviceListFragment.getFileItems();
this.fileItemIndex = fileItemIndex;
}
@Override
protected void onPreExecute() {
//fileItemIndex = deviceListFragment.getTotalFilesSent();
//TODO: Remove runnable for onPreExecute, onPostExecute, onProgressUpdate
deviceListFragment.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
transferProgressFragment.changeStatus(fileItemIndex, FileItem.SENDING);
}
});
}
@Override
protected String doInBackground(Uri... fileUris) {
Uri fileUri = fileUris[0];
Socket socket = new Socket();
try {
Log.d(LocalFileTransferActivity.TAG, "Opening client socket - ");
socket.bind(null);
for(int i = 0; i < 10000000; i++);
String hostAddress = deviceListFragment.getFileReceiverAddress().getHostAddress();
socket.connect((new InetSocketAddress(hostAddress, FILE_TRANSFER_PORT)), 15000);
Log.d(LocalFileTransferActivity.TAG, "Client socket - " + socket.isConnected());
OutputStream stream = socket.getOutputStream();
Log.d(LocalFileTransferActivity.TAG, "OutputStream found");
ContentResolver cr = context.getContentResolver();
Log.d(LocalFileTransferActivity.TAG, "ContentResolver obtained");
InputStream is = null;
try {
is = cr.openInputStream(fileUri);
Log.d(LocalFileTransferActivity.TAG, "Opened InputStream");
} catch (FileNotFoundException e) {
Log.d(LocalFileTransferActivity.TAG, e.toString());
}
DeviceListFragment.copyFile(is, stream);
Log.d(LocalFileTransferActivity.TAG, "Client: Data written");
} catch (IOException e) {
Log.e(LocalFileTransferActivity.TAG, e.getMessage());
} finally {
if (socket != null) {
if (socket.isConnected()) {
try {
socket.close();
} catch (IOException e) {
// Give up
e.printStackTrace();
}
}
}
return "";
}
}
@Override
protected void onPostExecute(String s) {
deviceListFragment.incrementTotalFilesSent();
deviceListFragment.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
transferProgressFragment.changeStatus(fileItemIndex, FileItem.SENT);
}
});
if(deviceListFragment.allFilesSent()) {
Toast.makeText(context, "All files transferred", Toast.LENGTH_SHORT).show();
deviceListFragment.getActivity().finish();
}
}
}
public static class FileServerAsyncTask extends AsyncTask<Void, Void, String> {
private Context context;
private DeviceListFragment deviceListFragment;
private TransferProgressFragment transferProgressFragment;
private int fileItemIndex;
public FileServerAsyncTask(Context context, DeviceListFragment deviceListFragment, TransferProgressFragment transferProgressFragment) {
this.context = context;
this.deviceListFragment = deviceListFragment;
this.transferProgressFragment = transferProgressFragment;
}
@Override
protected String doInBackground(Void... voids) {
try {
ServerSocket serverSocket = new ServerSocket(FILE_TRANSFER_PORT);
Log.d(LocalFileTransferActivity.TAG, "Server: Socket opened at " + FILE_TRANSFER_PORT);
final String KIWIX_ROOT = deviceListFragment.getZimStorageRootPath();
int totalFileCount = deviceListFragment.getTotalFiles();
for(int currentFile = 1; currentFile <= totalFileCount; currentFile++) {
Socket client = serverSocket.accept();
Log.d(LocalFileTransferActivity.TAG, "Server: Client connected");
fileItemIndex = currentFile-1;
deviceListFragment.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
transferProgressFragment.changeStatus(fileItemIndex, FileItem.SENDING);
}
});
ArrayList<FileItem> fileItems = deviceListFragment.getFileItems();
String incomingFileName = fileItems.get(currentFile-1).getFileName();
// TODO:? File selector, file not exists,
final File clientNoteFileLocation = new File(KIWIX_ROOT + incomingFileName);
File dirs = new File(clientNoteFileLocation.getParent());
if(!dirs.exists()) {
Log.d(LocalFileTransferActivity.TAG, "Parent creation result: "+dirs.mkdirs());
} else {
Log.d(LocalFileTransferActivity.TAG, "Parent directories exist");
}
Log.d(LocalFileTransferActivity.TAG, "File creation: "+clientNoteFileLocation.createNewFile());
Log.d(LocalFileTransferActivity.TAG, "Copying files");
InputStream inputStream = client.getInputStream();
copyFile(inputStream, new FileOutputStream(clientNoteFileLocation));
deviceListFragment.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
transferProgressFragment.changeStatus(fileItemIndex, FileItem.SENT);
}
});
deviceListFragment.incrementTotalFilesSent();
}
serverSocket.close();
return "";
} catch (IOException e) {
Log.e(LocalFileTransferActivity.TAG, e.getMessage());
return null;
}
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
Toast.makeText(context, "File transfer complete", Toast.LENGTH_LONG).show();
Log.d(LocalFileTransferActivity.TAG, "File transfer complete");
((LocalFileTransferActivity) deviceListFragment.getActivity()).closeLocalFileTransferActivity();
}
}
public static boolean copyFile(InputStream inputStream, OutputStream out) {
byte buf[] = new byte[1024];
int len;
try {
Log.d(LocalFileTransferActivity.TAG, "Copying to OutputStream...");
while ((len = inputStream.read(buf)) != -1) {
out.write(buf, 0, len);
}
out.close();
inputStream.close();
Log.d(LocalFileTransferActivity.TAG, "Both streams closed");
} catch (IOException e) {
Log.d(LocalFileTransferActivity.TAG, e.toString());
return false;
}
return true;
}
}

View File

@ -1,9 +1,10 @@
package org.kiwix.kiwixmobile.zim_manager.local_file_transfer;
public class FileItem {
public static short TO_BE_SENT = -1;
public static short SENDING = 0;
public static short SENT = +1;
public static final short TO_BE_SENT = -1;
public static final short SENDING = 0;
public static final short SENT = +1;
public static final short ERROR = +2;
private String fileName = "";
private short fileStatus = TO_BE_SENT;

View File

@ -42,10 +42,16 @@ public class FileListAdapter extends RecyclerView.Adapter<FileListAdapter.FileVi
holder.statusImage.setVisibility(View.GONE);
holder.progressBar.setVisibility(View.VISIBLE);
} else if(fileItem.getFileStatus() == FileItem.SENT) {
} else if(fileItem.getFileStatus() != FileItem.TO_BE_SENT){ // Icon for TO_BE_SENT is assigned by default in the item layout
holder.progressBar.setVisibility(View.GONE);
holder.statusImage.setImageResource(R.drawable.ic_baseline_check_24px);
switch (fileItem.getFileStatus()) {
case FileItem.SENT : holder.statusImage.setImageResource(R.drawable.ic_baseline_check_24px);
break;
case FileItem.ERROR: holder.statusImage.setImageResource(R.drawable.ic_baseline_error_24px);
break;
}
holder.statusImage.setVisibility(View.VISIBLE);
}
}

View File

@ -70,7 +70,7 @@ public class FileTransferService extends IntentService {
} catch (FileNotFoundException e) {
Log.d(LocalFileTransferActivity.TAG, e.toString());
}
DeviceListFragment.copyFile(is, stream);
DeviceListFragment.copyToOutputStream(is, stream);
Log.d(LocalFileTransferActivity.TAG, "Client: Data written");
} catch (IOException e) {
Log.e(LocalFileTransferActivity.TAG, e.getMessage());

View File

@ -63,7 +63,7 @@ public class LocalFileTransferActivity extends AppCompatActivity implements Wifi
@Inject
SharedPreferenceUtil sharedPreferenceUtil;
private ArrayList<Uri> fileURIArrayList; // For sender device, stores URIs of files to be transferred
private ArrayList<Uri> fileUriArrayList; // For sender device, stores Uris of files to be transferred
private Boolean fileSendingDevice = false;// Whether the device is the file sender or not
@ -86,15 +86,15 @@ public class LocalFileTransferActivity extends AppCompatActivity implements Wifi
KiwixApplication.getApplicationComponent().inject(this);
/*
* Presence of file URIs decides whether the device with the activity open is a sender or receiver:
* Presence of file Uris decides whether the device with the activity open is a sender or receiver:
* - On the sender device, this activity is started from the app chooser post selection
* of files to share in the Library
* - On the receiver device, the activity is started directly from within the 'Get Content'
* activity, without any file URIs
* activity, without any file Uris
* */
Intent filesIntent = getIntent();
fileURIArrayList = filesIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
if(fileURIArrayList != null && fileURIArrayList.size() > 0) {
fileUriArrayList = filesIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
if(fileUriArrayList != null && fileUriArrayList.size() > 0) {
setDeviceAsFileSender();
}
@ -186,8 +186,8 @@ public class LocalFileTransferActivity extends AppCompatActivity implements Wifi
return fileSendingDevice;
}
public @NonNull ArrayList<Uri> getFileURIArrayList() {
return fileURIArrayList;
public @NonNull ArrayList<Uri> getFileUriArrayList() {
return fileUriArrayList;
}
public void setWifiP2pEnabled(boolean wifiP2pEnabled) {
@ -276,7 +276,7 @@ public class LocalFileTransferActivity extends AppCompatActivity implements Wifi
}
@Override
public void connect(final WifiP2pDevice peerDevice) {
public void connect(@NonNull final WifiP2pDevice peerDevice) {
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = peerDevice.deviceAddress;
config.wps.setup = WpsInfo.PBC;

View File

@ -0,0 +1,138 @@
package org.kiwix.kiwixmobile.zim_manager.local_file_transfer;
import android.net.Uri;
import android.net.wifi.p2p.WifiP2pInfo;
import android.os.AsyncTask;
import android.util.Log;
import org.kiwix.kiwixmobile.BuildConfig;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import static org.kiwix.kiwixmobile.zim_manager.local_file_transfer.DeviceListFragment.PEER_HANDSHAKE_PORT;
import static org.kiwix.kiwixmobile.zim_manager.local_file_transfer.DeviceListFragment.getFileName;
import static org.kiwix.kiwixmobile.zim_manager.local_file_transfer.FileItem.TO_BE_SENT;
/**
* Helper class for the local file sharing module, used in {@link DeviceListFragment}.
*
* Once two peer devices are connected through wifi direct, this task is executed to perform a
* handshake between the two devices. The purpose of the handshake is to allow the file sending
* device to obtain the IP address of the file receiving device (When the file sending device
* is the wifi direct group owner, it doesn't have the IP address of the peer device by default).
*
* After obtaining the IP address, the sender also shares metadata regarding the file transfer
* (no. of files & their names) with the receiver. Finally, the onPostExecute() of the sender
* initiates the file transfer through {@link SenderDeviceAsyncTask} on the sender and using
* {@link ReceiverDeviceAsyncTask} on the receiver.
* */
class PeerGroupHandshakeAsyncTask extends AsyncTask<Void, Void, InetAddress> {
private static final String TAG = "PeerGrpHndshakeAsyncTsk";
private final String HANDSHAKE_MESSAGE = "Request Kiwix File Sharing";
private DeviceListFragment deviceListFragment;
private WifiP2pInfo groupInfo;
public PeerGroupHandshakeAsyncTask(DeviceListFragment deviceListFragment, WifiP2pInfo groupInfo) {
this.deviceListFragment = deviceListFragment;
this.groupInfo = groupInfo;
}
@Override
protected InetAddress doInBackground(Void... voids) {
try {
if(groupInfo.groupFormed && groupInfo.isGroupOwner) {
ServerSocket serverSocket = new ServerSocket(PEER_HANDSHAKE_PORT);
serverSocket.setReuseAddress(true);
Socket server = serverSocket.accept();
ObjectInputStream objectInputStream = new ObjectInputStream(server.getInputStream());
Object object = objectInputStream.readObject();
// Verify that the peer trying to communicate is a kiwix app intending to transfer files
if (object.getClass().equals(String.class) && object.equals(HANDSHAKE_MESSAGE)) {
if(BuildConfig.DEBUG) Log.d(TAG, "Client IP address: "+ server.getInetAddress());
exchangeFileTransferMetadata(server.getOutputStream(), server.getInputStream());
server.close();
return server.getInetAddress();
}
}
else if(groupInfo.groupFormed) { //&& !groupInfo.isGroupOwner
Socket client = new Socket();
client.setReuseAddress(true);
client.connect((new InetSocketAddress(groupInfo.groupOwnerAddress.getHostAddress(), PEER_HANDSHAKE_PORT)), 15000);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(client.getOutputStream());
objectOutputStream.writeObject(HANDSHAKE_MESSAGE); // Send message for the peer device to verify
exchangeFileTransferMetadata(client.getOutputStream(), client.getInputStream());
client.close();
return groupInfo.groupOwnerAddress;
}
} catch (Exception ex) {
ex.printStackTrace();
}
return null; // So a null is only returned in case of an exception
}
private void exchangeFileTransferMetadata(OutputStream outputStream, InputStream inputStream) {
try {
if(deviceListFragment.isFileSender()) {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
// Send total number of files which will be transferred
objectOutputStream.writeObject(""+deviceListFragment.getTotalFilesForTransfer());
ArrayList<Uri> fileUriList = deviceListFragment.getFileUriList();
for (Uri fileUri : fileUriList) { // Send the names of each of those files, in order
objectOutputStream.writeObject(getFileName(fileUri));
}
}
else { // Device is not the file sender
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
// Read the number of files
Object totalFilesObject = objectInputStream.readObject();
if(totalFilesObject.getClass().equals(String.class)) {
int total = Integer.parseInt((String) totalFilesObject);
deviceListFragment.setTotalFilesForTransfer(total);
ArrayList<FileItem> fileItems = new ArrayList<>();
for (int i = 0; i < total; i++) { // Read names of each of those files, in order
Object fileNameObject = objectInputStream.readObject();
if(fileNameObject.getClass().equals(String.class)) {
fileItems.add(new FileItem((String) fileNameObject, TO_BE_SENT));
}
}
deviceListFragment.setFileItems(fileItems);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onPostExecute(InetAddress inetAddress) {
deviceListFragment.setClientAddress(inetAddress);
}
}

View File

@ -0,0 +1,105 @@
package org.kiwix.kiwixmobile.zim_manager.local_file_transfer;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;
import org.kiwix.kiwixmobile.BuildConfig;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import static org.kiwix.kiwixmobile.zim_manager.local_file_transfer.DeviceListFragment.FILE_TRANSFER_PORT;
import static org.kiwix.kiwixmobile.zim_manager.local_file_transfer.DeviceListFragment.copyToOutputStream;
/**
* Helper class for the local file sharing module, used in {@link DeviceListFragment}.
*
* Once the handshake has successfully taken place, this async-task is used to receive files from
* the sender device on the FILE_TRANSFER_PORT port. No. of files to be received (and their names)
* are learnt beforehand during the handshake.
*
* A single Task is used for the entire file transfer (the server socket accepts connections as
* many times as the no. of files).
* */
class ReceiverDeviceAsyncTask extends AsyncTask<Void, Short, Boolean> {
private static final String TAG = "ReceiverDeviceAsyncTask";
private DeviceListFragment deviceListFragment;
private TransferProgressFragment transferProgressFragment;
private int fileItemIndex;
public ReceiverDeviceAsyncTask(DeviceListFragment deviceListFragment, TransferProgressFragment transferProgressFragment) {
this.deviceListFragment = deviceListFragment;
this.transferProgressFragment = transferProgressFragment;
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
ServerSocket serverSocket = new ServerSocket(FILE_TRANSFER_PORT);
if(BuildConfig.DEBUG) Log.d(TAG, "Server: Socket opened at " + FILE_TRANSFER_PORT);
final String KIWIX_ROOT = deviceListFragment.getZimStorageRootPath();
int totalFileCount = deviceListFragment.getTotalFilesForTransfer();
for(int currentFile = 1; currentFile <= totalFileCount; currentFile++) {
Socket client = serverSocket.accept();
if(BuildConfig.DEBUG) Log.d(TAG, "Server: Client connected for file " + currentFile);
fileItemIndex = currentFile-1;
publishProgress(FileItem.SENDING);
ArrayList<FileItem> fileItems = deviceListFragment.getFileItems();
String incomingFileName = fileItems.get(fileItemIndex).getFileName();
final File clientNoteFileLocation = new File(KIWIX_ROOT + incomingFileName);
File dirs = new File(clientNoteFileLocation.getParent());
if(!dirs.exists() && !dirs.mkdirs()) {
Log.d(TAG, "ERROR: Required parent directories couldn't be created");
return false;
}
boolean fileCreated = clientNoteFileLocation.createNewFile();
if(BuildConfig.DEBUG) Log.d(TAG, "File creation: "+ fileCreated);
copyToOutputStream(client.getInputStream(), new FileOutputStream(clientNoteFileLocation));
publishProgress(FileItem.SENT);
deviceListFragment.incrementTotalFilesSent();
}
serverSocket.close();
return true; // Returned in case of a succesful file transfer
} catch (IOException e) {
Log.e(TAG, e.getMessage());
return false; // Returned when an error was encountered during transfer
}
}
@Override
protected void onProgressUpdate(Short... values) {
short fileStatus = values[0];
transferProgressFragment.changeStatus(fileItemIndex, fileStatus);
}
@Override
protected void onPostExecute(Boolean allFilesReceived) {
if(BuildConfig.DEBUG) Log.d(TAG, "File transfer complete");
if(allFilesReceived) {
Toast.makeText(deviceListFragment.getActivity(), "File transfer complete", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(deviceListFragment.getActivity(), "An error was encountered during transfer", Toast.LENGTH_LONG).show();
}
((LocalFileTransferActivity) deviceListFragment.getActivity()).closeLocalFileTransferActivity();
}
}

View File

@ -0,0 +1,108 @@
package org.kiwix.kiwixmobile.zim_manager.local_file_transfer;
import android.content.ContentResolver;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.Toast;
import org.kiwix.kiwixmobile.BuildConfig;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import static org.kiwix.kiwixmobile.zim_manager.local_file_transfer.DeviceListFragment.FILE_TRANSFER_PORT;
import static org.kiwix.kiwixmobile.zim_manager.local_file_transfer.DeviceListFragment.getFileName;
/**
* Helper class for the local file sharing module, used in {@link DeviceListFragment}.
*
* Once the handshake between the two connected devices has taked place, this async-task is used
* on the sender device to transfer the file to the receiver device at the FILE_TRANSFER_PORT port.
*
* It takes in the uri of a single file, and copies all the bytes from input stream of the file to
* the output stream of the receiver device. Also changes the status of the corresponding FileItem
* on the list of files for transfer in {@link TransferProgressFragment}.
*
* A new task is created by the sender for every file to be transferred
* */
class SenderDeviceAsyncTask extends AsyncTask<Uri, Void, Boolean> {
private static final String TAG = "SenderDeviceAsyncTask";
private DeviceListFragment deviceListFragment;
private TransferProgressFragment transferProgressFragment;
private int fileItemIndex;
public SenderDeviceAsyncTask(DeviceListFragment deviceListFragment, TransferProgressFragment transferProgressFragment, int fileItemIndex) {
this.deviceListFragment = deviceListFragment;
this.transferProgressFragment = transferProgressFragment;
this.fileItemIndex = fileItemIndex;
}
@Override
protected void onPreExecute() {
transferProgressFragment.changeStatus(fileItemIndex, FileItem.SENDING);
}
@Override
protected Boolean doInBackground(Uri... fileUris) {
Uri fileUri = fileUris[0]; // Uri of file to be transferred
Socket socket = new Socket(); // Represents the sender device
boolean fileSendSuccessful; // Whether this task was successful in sending the file
try {
socket.bind(null);
String hostAddress = deviceListFragment.getFileReceiverDeviceAddress().getHostAddress();
socket.connect((new InetSocketAddress(hostAddress, FILE_TRANSFER_PORT)), 15000);
if(BuildConfig.DEBUG) Log.d(TAG, "Sender socket - " + socket.isConnected());
OutputStream socketOutputStream = socket.getOutputStream();
ContentResolver contentResolver = deviceListFragment.getActivity().getContentResolver();
InputStream fileInputStream = contentResolver.openInputStream(fileUri);
DeviceListFragment.copyToOutputStream(fileInputStream, socketOutputStream);
if(BuildConfig.DEBUG) Log.d(TAG, "Sender: Data written");
fileSendSuccessful = true;
} catch (IOException e) {
Log.e(TAG, e.getMessage());
fileSendSuccessful = false;
} finally {
if (socket.isConnected()) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return fileSendSuccessful;
}
@Override
protected void onPostExecute(Boolean fileSendSuccessful) {
deviceListFragment.incrementTotalFilesSent();
if(fileSendSuccessful) {
transferProgressFragment.changeStatus(fileItemIndex, FileItem.SENT);
} else {
Toast.makeText(deviceListFragment.getActivity(), "Error sending file "+ getFileName(deviceListFragment.getFileUriList().get(fileItemIndex)), Toast.LENGTH_SHORT).show();
transferProgressFragment.changeStatus(fileItemIndex, FileItem.ERROR);
}
if(deviceListFragment.allFilesSent()) {
Toast.makeText(deviceListFragment.getActivity(), "All files transferred", Toast.LENGTH_SHORT).show();
deviceListFragment.getActivity().finish();
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>