diff --git a/app/detekt_baseline.xml b/app/detekt_baseline.xml index a2dff3391..5a94ce9eb 100644 --- a/app/detekt_baseline.xml +++ b/app/detekt_baseline.xml @@ -34,6 +34,7 @@ PackageNaming:ShareFiles.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects PackageNaming:SimplePageChangeListener.kt$package org.kiwix.kiwixmobile.zim_manager PackageNaming:StartMultiSelection.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects + PackageNaming:WifiDirectManager.kt$package org.kiwix.kiwixmobile.local_file_transfer PackageNaming:WifiP2pDelegate.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter PackageNaming:WifiP2pViewHolder.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter PackageNaming:WifiPeerListAdapter.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter @@ -50,6 +51,7 @@ TooManyFunctions:KiwixActivityComponent.kt$KiwixActivityComponent$KiwixActivityComponent TooManyFunctions:KiwixMainActivity.kt$KiwixMainActivity$KiwixMainActivity TooManyFunctions:LibraryFragment.kt$LibraryFragment$LibraryFragment + TooManyFunctions:WifiDirectManager.kt$WifiDirectManager$WifiDirectManager TooManyFunctions:ZimFileSelectFragment.kt$ZimFileSelectFragment$ZimFileSelectFragment TooManyFunctions:ZimHostActivity.kt$ZimHostActivity$ZimHostActivity TooManyFunctions:ZimManageViewModel.kt$ZimManageViewModel$ZimManageViewModel diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/WifiDirectManager.java b/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/WifiDirectManager.java deleted file mode 100644 index 45f840ba5..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/WifiDirectManager.java +++ /dev/null @@ -1,482 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2019 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.kiwix.kiwixmobile.local_file_transfer; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.IntentFilter; -import android.net.Uri; -import android.net.wifi.WpsInfo; -import android.net.wifi.p2p.WifiP2pConfig; -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.Build; -import android.util.Log; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.util.ArrayList; -import javax.inject.Inject; -import kotlin.Unit; -import kotlin.jvm.functions.Function0; -import org.kiwix.kiwixmobile.core.BuildConfig; -import org.kiwix.kiwixmobile.core.R; -import org.kiwix.kiwixmobile.core.utils.AlertDialogShower; -import org.kiwix.kiwixmobile.core.utils.KiwixDialog; -import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil; - -import static android.os.Looper.getMainLooper; -import static org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.ERROR; -import static org.kiwix.kiwixmobile.local_file_transfer.LocalFileTransferActivity.showToast; - -/** - * Manager for the Wifi-P2p API, used in the local file transfer module - */ -@SuppressWarnings("MissingPermission") -public class WifiDirectManager - implements WifiP2pManager.ChannelListener, WifiP2pManager.PeerListListener, - WifiP2pManager.ConnectionInfoListener, - KiwixWifiP2pBroadcastReceiver.P2pEventListener { - - private static final String TAG = "WifiDirectManager"; - public static int FILE_TRANSFER_PORT = 8008; - - private @NonNull Activity activity; - private @NonNull Callbacks callbacks; - - private SharedPreferenceUtil sharedPreferenceUtil; - private AlertDialogShower alertDialogShower; - - /* Variables related to the WiFi P2P API */ - private boolean isWifiP2pEnabled = false; // Whether WiFi has been enabled or not - private boolean shouldRetry = true; // Whether channel has retried connecting previously - - private WifiP2pManager manager; // Overall manager of Wifi p2p connections for the module - private WifiP2pManager.Channel channel; // Interface to the device's underlying wifi-p2p framework - - private BroadcastReceiver receiver = null; // For receiving the broadcasts given by above filter - - private WifiP2pInfo groupInfo; // Corresponds to P2P group formed between the two devices - - private WifiP2pDevice senderSelectedPeerDevice = null; - - private PeerGroupHandshakeAsyncTask peerGroupHandshakeAsyncTask; - private SenderDeviceAsyncTask senderDeviceAsyncTask; - private ReceiverDeviceAsyncTask receiverDeviceAsyncTask; - - private InetAddress selectedPeerDeviceInetAddress; - private InetAddress fileReceiverDeviceAddress; // IP address of the file receiving device - - private ArrayList filesForTransfer; - - private boolean isFileSender = false; // Whether the device is the file sender or not - private boolean hasSenderStartedConnection = false; - - @Inject - public WifiDirectManager(@NonNull Activity activity, - @NonNull SharedPreferenceUtil sharedPreferenceUtil, - @NonNull AlertDialogShower alertDialogShower) { - this.activity = activity; - this.callbacks = (Callbacks) activity; - this.sharedPreferenceUtil = sharedPreferenceUtil; - this.alertDialogShower = alertDialogShower; - } - - /* Initialisations for using the WiFi P2P API */ - public void startWifiDirectManager(@Nullable ArrayList filesForTransfer) { - this.filesForTransfer = filesForTransfer; - this.isFileSender = (filesForTransfer != null && filesForTransfer.size() > 0); - - manager = (WifiP2pManager) activity.getSystemService(Context.WIFI_P2P_SERVICE); - channel = manager.initialize(activity, getMainLooper(), null); - registerWifiDirectBroadcastReceiver(); - } - - public void registerWifiDirectBroadcastReceiver() { - receiver = new KiwixWifiP2pBroadcastReceiver(this); - - // For specifying broadcasts (of the P2P API) that the module needs to respond to - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION); - intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION); - intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION); - intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION); - - activity.registerReceiver(receiver, intentFilter); - } - - public void unregisterWifiDirectBroadcastReceiver() { - activity.unregisterReceiver(receiver); - } - - public void discoverPeerDevices() { - manager.discoverPeers(channel, new WifiP2pManager.ActionListener() { - @Override - public void onSuccess() { - displayToast(R.string.discovery_initiated, Toast.LENGTH_SHORT); - } - - @Override - public void onFailure(int reason) { - String errorMessage = getErrorMessage(reason); - Log.d(TAG, activity.getString(R.string.discovery_failed) + ": " + errorMessage); - displayToast(R.string.discovery_failed, Toast.LENGTH_SHORT); - } - }); - } - - /* From KiwixWifiP2pBroadcastReceiver.P2pEventListener callback-interface*/ - @Override - public void onWifiP2pStateChanged(boolean isEnabled) { - this.isWifiP2pEnabled = isEnabled; - - if (!isWifiP2pEnabled) { - displayToast(R.string.discovery_needs_wifi, Toast.LENGTH_SHORT); - callbacks.onConnectionToPeersLost(); - } - - Log.d(TAG, "WiFi P2P state changed - " + isWifiP2pEnabled); - } - - @Override - public void onPeersChanged() { - /* List of available peers has changed, so request & use the new list through - * PeerListListener.requestPeers() callback */ - manager.requestPeers(channel, this); - Log.d(TAG, "P2P peers changed"); - } - - @Override - public void onConnectionChanged(boolean isConnected) { - if (isConnected) { - // Request connection info about the wifi p2p group formed upon connection - manager.requestConnectionInfo(channel, this); - } else { - // Not connected after connection change -> Disconnected - callbacks.onConnectionToPeersLost(); - } - } - - @Override - public void onDeviceChanged(@Nullable WifiP2pDevice userDevice) { - // Update UI with wifi-direct details about the user device - callbacks.onUserDeviceDetailsAvailable(userDevice); - } - - /* From WifiP2pManager.ChannelListener interface */ - @Override - public void onChannelDisconnected() { - // Upon disconnection, retry one more time - if (shouldRetry) { - Log.d(TAG, "Channel lost, trying again"); - callbacks.onConnectionToPeersLost(); - shouldRetry = false; - manager.initialize(activity, getMainLooper(), this); - } else { - displayToast(R.string.severe_loss_error, Toast.LENGTH_LONG); - } - } - - /* From WifiP2pManager.PeerListListener callback-interface */ - @Override - public void onPeersAvailable(@NonNull WifiP2pDeviceList peers) { - callbacks.updateListOfAvailablePeers(peers); - } - - /* From WifiP2pManager.ConnectionInfoListener callback-interface */ - @Override - public void onConnectionInfoAvailable(@NonNull WifiP2pInfo groupInfo) { - /* Devices have successfully connected, and 'info' holds information about the wifi p2p group formed */ - this.groupInfo = groupInfo; - performHandshakeWithSelectedPeerDevice(); - } - - /* Helper methods */ - public boolean isWifiP2pEnabled() { - return isWifiP2pEnabled; - } - - public boolean isGroupFormed() { - return groupInfo.groupFormed; - } - - public boolean isGroupOwner() { - return groupInfo.isGroupOwner; - } - - public @NonNull InetAddress getGroupOwnerAddress() { - return groupInfo.groupOwnerAddress; - } - - public void sendToDevice(@NonNull WifiP2pDevice senderSelectedPeerDevice) { - /* Connection can only be initiated by user of the sender device, & only when transfer has not been started */ - if (!isFileSender || hasSenderStartedConnection) { - return; - } - - this.senderSelectedPeerDevice = senderSelectedPeerDevice; - - alertDialogShower.show( - new KiwixDialog.FileTransferConfirmation(senderSelectedPeerDevice.deviceName), - new Function0() { - @Override public Unit invoke() { - hasSenderStartedConnection = true; - connect(); - displayToast(R.string.performing_handshake, Toast.LENGTH_LONG); - return Unit.INSTANCE; - } - }); - } - - public void connect() { - if (senderSelectedPeerDevice == null) { - Log.d(TAG, "No device set as selected"); - } - - WifiP2pConfig config = new WifiP2pConfig(); - config.deviceAddress = senderSelectedPeerDevice.deviceAddress; - config.wps.setup = WpsInfo.PBC; - - manager.connect(channel, config, new WifiP2pManager.ActionListener() { - @Override - public void onSuccess() { - // UI updated from broadcast receiver - } - - @Override - public void onFailure(int reason) { - String errorMessage = getErrorMessage(reason); - Log.d(TAG, activity.getString(R.string.connection_failed) + ": " + errorMessage); - displayToast(R.string.connection_failed, Toast.LENGTH_LONG); - } - }); - } - - public void performHandshakeWithSelectedPeerDevice() { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Starting handshake"); - } - peerGroupHandshakeAsyncTask = new PeerGroupHandshakeAsyncTask(this); - peerGroupHandshakeAsyncTask.execute(); - } - - public boolean isFileSender() { - return isFileSender; - } - - public int getTotalFilesForTransfer() { - return filesForTransfer.size(); - } - - public @NonNull ArrayList getFilesForTransfer() { - return filesForTransfer; - } - - public void setFilesForTransfer(@NonNull ArrayList fileItems) { - this.filesForTransfer = fileItems; - } - - public @NonNull String getZimStorageRootPath() { - return (sharedPreferenceUtil.getPrefStorage() + "/Kiwix/"); - } - - public @NonNull InetAddress getFileReceiverDeviceAddress() { - return fileReceiverDeviceAddress; - } - - public static void copyToOutputStream(@NonNull InputStream inputStream, - @NonNull OutputStream outputStream) throws IOException { - byte[] bufferForBytes = new byte[1024]; - int bytesRead; - - 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"); - } - - public void setClientAddress(@Nullable InetAddress clientAddress) { - if (clientAddress == null) { - // null is returned only in case of a failed handshake - displayToast(R.string.device_not_cooperating, Toast.LENGTH_LONG); - callbacks.onFileTransferComplete(); - return; - } - - // If control reaches here, means handshake was successful - selectedPeerDeviceInetAddress = clientAddress; - startFileTransfer(); - } - - private void startFileTransfer() { - if (isGroupFormed()) { - if (isFileSender) { - Log.d(LocalFileTransferActivity.TAG, "Starting file transfer"); - - fileReceiverDeviceAddress = - (isGroupOwner()) ? selectedPeerDeviceInetAddress : getGroupOwnerAddress(); - - displayToast(R.string.preparing_files, Toast.LENGTH_LONG); - senderDeviceAsyncTask = new SenderDeviceAsyncTask(this, activity); - senderDeviceAsyncTask.execute(filesForTransfer.toArray(new FileItem[0])); - } else { - callbacks.onFilesForTransferAvailable(filesForTransfer); - - receiverDeviceAsyncTask = new ReceiverDeviceAsyncTask(this); - receiverDeviceAsyncTask.execute(); - } - } - } - - public void changeStatus(int itemIndex, @FileItem.FileStatus int status) { - filesForTransfer.get(itemIndex).setFileStatus(status); - callbacks.onFileStatusChanged(itemIndex); - - if (status == ERROR) { - displayToast(R.string.error_transferring, filesForTransfer.get(itemIndex).getFileName(), - Toast.LENGTH_SHORT); - } - } - - private void cancelAsyncTasks(AsyncTask... tasks) { - for (AsyncTask asyncTask : tasks) { - if (asyncTask != null) { - asyncTask.cancel(true); - } - } - } - - public void stopWifiDirectManager() { - cancelAsyncTasks(peerGroupHandshakeAsyncTask, senderDeviceAsyncTask, receiverDeviceAsyncTask); - - if (isFileSender) { - closeChannel(); - } else { - disconnect(); - } - - unregisterWifiDirectBroadcastReceiver(); - } - - public void disconnect() { - manager.removeGroup(channel, new WifiP2pManager.ActionListener() { - - @Override - public void onFailure(int reasonCode) { - Log.d(TAG, "Disconnect failed. Reason: " + reasonCode); - closeChannel(); - } - - @Override - public void onSuccess() { - Log.d(TAG, "Disconnect successful"); - closeChannel(); - } - }); - } - - private void closeChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - channel.close(); - } - } - - public @NonNull String getErrorMessage(int reason) { - switch (reason) { - case WifiP2pManager.ERROR: - return "Internal error"; - case WifiP2pManager.BUSY: - return "Framework busy, unable to service request"; - case WifiP2pManager.P2P_UNSUPPORTED: - return "P2P unsupported on this device"; - - default: - return ("Unknown error code - " + reason); - } - } - - public static @NonNull String getDeviceStatus(int status) { - - if (BuildConfig.DEBUG) Log.d(TAG, "Peer Status: " + status); - switch (status) { - case WifiP2pDevice.AVAILABLE: - return "Available"; - case WifiP2pDevice.INVITED: - return "Invited"; - case WifiP2pDevice.CONNECTED: - return "Connected"; - case WifiP2pDevice.FAILED: - return "Failed"; - case WifiP2pDevice.UNAVAILABLE: - return "Unavailable"; - - default: - return "Unknown"; - } - } - - public static @NonNull String getFileName(@NonNull Uri fileUri) { - String fileUriString = fileUri.toString(); - // Returns text after location of last slash in the file path - return fileUriString.substring(fileUriString.lastIndexOf('/') + 1); - } - - public void displayToast(int stringResourceId, @NonNull String templateValue, int duration) { - showToast(activity, activity.getString(stringResourceId, templateValue), duration); - } - - public void displayToast(int stringResourceId, int duration) { - showToast(activity, stringResourceId, duration); - } - - public void onFileTransferAsyncTaskComplete(boolean wereAllFilesTransferred) { - if (wereAllFilesTransferred) { - displayToast(R.string.file_transfer_complete, Toast.LENGTH_LONG); - } else { - displayToast(R.string.error_during_transfer, Toast.LENGTH_LONG); - } - callbacks.onFileTransferComplete(); - } - - public interface Callbacks { - void onUserDeviceDetailsAvailable(@Nullable WifiP2pDevice userDevice); - - void onConnectionToPeersLost(); - - void updateListOfAvailablePeers(@NonNull WifiP2pDeviceList peers); - - void onFilesForTransferAvailable(@NonNull ArrayList filesForTransfer); - - void onFileStatusChanged(int itemIndex); - - void onFileTransferComplete(); - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/WifiDirectManager.kt b/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/WifiDirectManager.kt new file mode 100644 index 000000000..7777331fc --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/WifiDirectManager.kt @@ -0,0 +1,389 @@ +/* + * Kiwix Android + * Copyright (c) 2019 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package org.kiwix.kiwixmobile.local_file_transfer + +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.IntentFilter +import android.net.Uri +import android.net.wifi.WpsInfo +import android.net.wifi.p2p.WifiP2pConfig +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.net.wifi.p2p.WifiP2pManager.ActionListener +import android.net.wifi.p2p.WifiP2pManager.Channel +import android.net.wifi.p2p.WifiP2pManager.ChannelListener +import android.net.wifi.p2p.WifiP2pManager.ConnectionInfoListener +import android.net.wifi.p2p.WifiP2pManager.PeerListListener +import android.os.AsyncTask +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.os.Looper +import android.util.Log +import android.widget.Toast +import org.kiwix.kiwixmobile.R +import org.kiwix.kiwixmobile.core.BuildConfig +import org.kiwix.kiwixmobile.core.extensions.toast +import org.kiwix.kiwixmobile.core.utils.AlertDialogShower +import org.kiwix.kiwixmobile.core.utils.KiwixDialog.FileTransferConfirmation +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus +import org.kiwix.kiwixmobile.local_file_transfer.KiwixWifiP2pBroadcastReceiver.P2pEventListener +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.InetAddress +import java.util.ArrayList +import javax.inject.Inject + +/** + * Manager for the Wifi-P2p API, used in the local file transfer module + */ +@SuppressWarnings("MissingPermission") +class WifiDirectManager @Inject constructor( + private val activity: Activity, + private val sharedPreferenceUtil: SharedPreferenceUtil, + private val alertDialogShower: AlertDialogShower +) : ChannelListener, PeerListListener, ConnectionInfoListener, P2pEventListener { + private val callbacks = activity as Callbacks + + /* Helper methods */ + /* Variables related to the WiFi P2P API */ + // Whether WiFi has been enabled or not + var isWifiP2pEnabled = false + private set + + // Whether channel has retried connecting previously + private var shouldRetry = true + + // Overall manager of Wifi p2p connections for the module + private lateinit var manager: WifiP2pManager + + // Interface to the device's underlying wifi-p2p framework + private lateinit var channel: Channel + + // For receiving the broadcasts given by above filter + private lateinit var receiver: BroadcastReceiver + + // Corresponds to P2P group formed between the two devices + private lateinit var groupInfo: WifiP2pInfo + private lateinit var senderSelectedPeerDevice: WifiP2pDevice + private var peerGroupHandshakeAsyncTask: PeerGroupHandshakeAsyncTask? = null + private var senderDeviceAsyncTask: SenderDeviceAsyncTask? = null + private var receiverDeviceAsyncTask: ReceiverDeviceAsyncTask? = null + private lateinit var selectedPeerDeviceInetAddress: InetAddress + + // IP address of the file receiving device + private lateinit var fileReceiverDeviceAddress: InetAddress + private lateinit var filesForTransfer: ArrayList + + // Whether the device is the file sender or not + var isFileSender = false + private set + private var hasSenderStartedConnection = false + + /* Initialisations for using the WiFi P2P API */ + fun startWifiDirectManager(filesForTransfer: ArrayList) { + this.filesForTransfer = filesForTransfer + isFileSender = filesForTransfer.isNotEmpty() + manager = activity.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager + channel = manager.initialize(activity, Looper.getMainLooper(), null) + registerWifiDirectBroadcastReceiver() + } + + private fun registerWifiDirectBroadcastReceiver() { + receiver = KiwixWifiP2pBroadcastReceiver(this) + + // For specifying broadcasts (of the P2P API) that the module needs to respond to + val intentFilter = IntentFilter() + intentFilter.apply { + addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION) + addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION) + } + activity.registerReceiver(receiver, intentFilter) + } + + private fun unregisterWifiDirectBroadcastReceiver() = activity.unregisterReceiver(receiver) + + fun discoverPeerDevices() { + manager.discoverPeers(channel, object : ActionListener { + override fun onSuccess() { + activity.toast(R.string.discovery_initiated, Toast.LENGTH_SHORT) + } + + override fun onFailure(reason: Int) { + Log.d( + TAG, "${activity.getString(R.string.discovery_failed)}: " + + getErrorMessage(reason) + ) + activity.toast(R.string.discovery_failed, Toast.LENGTH_SHORT) + } + }) + } + + /* From KiwixWifiP2pBroadcastReceiver.P2pEventListener callback-interface*/ + override fun onWifiP2pStateChanged(isEnabled: Boolean) { + isWifiP2pEnabled = isEnabled + if (!isWifiP2pEnabled) { + activity.toast(R.string.discovery_needs_wifi, Toast.LENGTH_SHORT) + callbacks.onConnectionToPeersLost() + } + Log.d(TAG, "WiFi P2P state changed - $isWifiP2pEnabled") + } + + override fun onPeersChanged() { + /* List of available peers has changed, so request & use the new list through + * PeerListListener.requestPeers() callback */ + manager.requestPeers(channel, this) + Log.d(TAG, "P2P peers changed") + } + + override fun onConnectionChanged(isConnected: Boolean) { + if (isConnected) { + // Request connection info about the wifi p2p group formed upon connection + manager.requestConnectionInfo(channel, this) + } else { + // Not connected after connection change -> Disconnected + callbacks.onConnectionToPeersLost() + } + } + + // Update UI with wifi-direct details about the user device + override fun onDeviceChanged(userDevice: WifiP2pDevice?) = + callbacks.onUserDeviceDetailsAvailable(userDevice) + + /* From WifiP2pManager.ChannelListener interface */ + override fun onChannelDisconnected() { + // Upon disconnection, retry one more time + if (shouldRetry) { + Log.d(TAG, "Channel lost, trying again") + callbacks.onConnectionToPeersLost() + shouldRetry = false + manager.initialize(activity, Looper.getMainLooper(), this) + } else { + activity.toast(R.string.severe_loss_error, Toast.LENGTH_LONG) + } + } + + /* From WifiP2pManager.PeerListListener callback-interface */ + override fun onPeersAvailable(peers: WifiP2pDeviceList) = + callbacks.updateListOfAvailablePeers(peers) + + /* From WifiP2pManager.ConnectionInfoListener callback-interface */ + override fun onConnectionInfoAvailable(groupInfo: WifiP2pInfo) { + /* Devices have successfully connected, and 'info' holds information about the wifi p2p group formed */ + this.groupInfo = groupInfo + performHandshakeWithSelectedPeerDevice() + } + + val isGroupFormed: Boolean + get() = groupInfo.groupFormed + + val isGroupOwner: Boolean + get() = groupInfo.isGroupOwner + + val groupOwnerAddress: InetAddress + get() = groupInfo.groupOwnerAddress + + fun sendToDevice(senderSelectedPeerDevice: WifiP2pDevice) { + /* Connection can only be initiated by user of the sender device, & only when transfer has not been started */ + if (isFileSender && !hasSenderStartedConnection) { + this.senderSelectedPeerDevice = senderSelectedPeerDevice + alertDialogShower.show( + FileTransferConfirmation(senderSelectedPeerDevice.deviceName), { + hasSenderStartedConnection = true + connect() + activity.toast(R.string.performing_handshake, Toast.LENGTH_LONG) + }) + } + } + + private fun connect() { + val config = WifiP2pConfig().apply { + deviceAddress = senderSelectedPeerDevice.deviceAddress + wps.setup = WpsInfo.PBC + } + manager.connect(channel, config, object : ActionListener { + override fun onSuccess() { + // UI updated from broadcast receiver + } + + override fun onFailure(reason: Int) { + val errorMessage = getErrorMessage(reason) + Log.d(TAG, activity.getString(R.string.connection_failed) + ": " + errorMessage) + activity.toast(R.string.connection_failed, Toast.LENGTH_LONG) + } + }) + } + + private fun performHandshakeWithSelectedPeerDevice() { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Starting handshake") + } + peerGroupHandshakeAsyncTask = PeerGroupHandshakeAsyncTask(this) + .also { it.execute() } + } + + val totalFilesForTransfer: Int + get() = filesForTransfer.size + + fun getFilesForTransfer() = filesForTransfer + + fun setFilesForTransfer(fileItems: ArrayList) { + filesForTransfer = fileItems + } + + val zimStorageRootPath + get() = sharedPreferenceUtil.prefStorage + "/Kiwix/" + + fun getFileReceiverDeviceAddress() = fileReceiverDeviceAddress + + fun setClientAddress(clientAddress: InetAddress) { + + // If control reaches here, means handshake was successful + selectedPeerDeviceInetAddress = clientAddress + startFileTransfer() + } + + private fun startFileTransfer() { + if (isGroupFormed) { + if (isFileSender) { + Log.d(LocalFileTransferActivity.TAG, "Starting file transfer") + fileReceiverDeviceAddress = + if (isGroupOwner) selectedPeerDeviceInetAddress else groupOwnerAddress + activity.toast(R.string.preparing_files, Toast.LENGTH_LONG) + senderDeviceAsyncTask = SenderDeviceAsyncTask(this, activity).also { + it.execute() + } + } else { + callbacks.onFilesForTransferAvailable(filesForTransfer) + receiverDeviceAsyncTask = ReceiverDeviceAsyncTask(this).also { + it.execute() + } + } + } + } + + fun changeStatus(itemIndex: Int, @FileStatus status: Int) { + filesForTransfer[itemIndex].fileStatus = status + callbacks.onFileStatusChanged(itemIndex) + if (status == FileStatus.ERROR) { + displayToast( + R.string.error_transferring, filesForTransfer[itemIndex].fileName, + Toast.LENGTH_SHORT + ) + } + } + + private fun cancelAsyncTasks(vararg tasks: AsyncTask<*, *, *>?) = + tasks.forEach { + it?.cancel(true) + } + + fun stopWifiDirectManager() { + cancelAsyncTasks(peerGroupHandshakeAsyncTask, senderDeviceAsyncTask, receiverDeviceAsyncTask) + if (isFileSender) { + closeChannel() + } else { + disconnect() + } + unregisterWifiDirectBroadcastReceiver() + } + + private fun disconnect() { + manager.removeGroup(channel, object : ActionListener { + override fun onFailure(reasonCode: Int) { + Log.d(TAG, "Disconnect failed. Reason: $reasonCode") + closeChannel() + } + + override fun onSuccess() { + Log.d(TAG, "Disconnect successful") + closeChannel() + } + }) + } + + private fun closeChannel() { + if (VERSION.SDK_INT >= VERSION_CODES.O_MR1) { + channel.close() + } + } + + fun getErrorMessage(reason: Int): String { + return when (reason) { + WifiP2pManager.ERROR -> "Internal error" + WifiP2pManager.BUSY -> "Framework busy, unable to service request" + WifiP2pManager.P2P_UNSUPPORTED -> "P2P unsupported on this device" + else -> "Unknown error code - $reason" + } + } + + fun displayToast(stringResourceId: Int, templateValue: String, duration: Int) = + activity.toast(activity.getString(stringResourceId, templateValue), duration) + + fun onFileTransferAsyncTaskComplete(wereAllFilesTransferred: Boolean) { + if (wereAllFilesTransferred) { + activity.toast(R.string.file_transfer_complete, Toast.LENGTH_LONG) + } else { + activity.toast(R.string.error_during_transfer, Toast.LENGTH_LONG) + } + callbacks.onFileTransferComplete() + } + + interface Callbacks { + fun onUserDeviceDetailsAvailable(userDevice: WifiP2pDevice?) + fun onConnectionToPeersLost() + fun updateListOfAvailablePeers(peers: WifiP2pDeviceList) + fun onFilesForTransferAvailable(filesForTransfer: ArrayList) + fun onFileStatusChanged(itemIndex: Int) + fun onFileTransferComplete() + } + + companion object { + private const val TAG = "WifiDirectManager" + @JvmField var FILE_TRANSFER_PORT = 8008 + @JvmStatic @Throws(IOException::class) fun copyToOutputStream( + inputStream: InputStream, + outputStream: OutputStream + ) { + inputStream.use { input -> outputStream.use { output -> input.copyTo(output) } } + Log.d(LocalFileTransferActivity.TAG, "Both streams closed") + } + + @JvmStatic fun getDeviceStatus(status: Int): String { + if (BuildConfig.DEBUG) Log.d(TAG, "Peer Status: $status") + return when (status) { + WifiP2pDevice.AVAILABLE -> "Available" + WifiP2pDevice.INVITED -> "Invited" + WifiP2pDevice.CONNECTED -> "Connected" + WifiP2pDevice.FAILED -> "Failed" + WifiP2pDevice.UNAVAILABLE -> "Unavailable" + else -> "Unknown" + } + } + + // Returns text after location of last slash in the file path + @JvmStatic fun getFileName(fileUri: Uri) = "$fileUri".substringAfterLast('/') + } +}