mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -04:00
Merge pull request #2011 from yash1ts/#1835-wifi-direct-manager-to-kotlin
WifiDirectManager to Kotlin
This commit is contained in:
commit
20b5d187c8
@ -34,6 +34,7 @@
|
||||
<ID>PackageNaming:ShareFiles.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects</ID>
|
||||
<ID>PackageNaming:SimplePageChangeListener.kt$package org.kiwix.kiwixmobile.zim_manager</ID>
|
||||
<ID>PackageNaming:StartMultiSelection.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects</ID>
|
||||
<ID>PackageNaming:WifiDirectManager.kt$package org.kiwix.kiwixmobile.local_file_transfer</ID>
|
||||
<ID>PackageNaming:WifiP2pDelegate.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter</ID>
|
||||
<ID>PackageNaming:WifiP2pViewHolder.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter</ID>
|
||||
<ID>PackageNaming:WifiPeerListAdapter.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter</ID>
|
||||
@ -50,6 +51,7 @@
|
||||
<ID>TooManyFunctions:KiwixActivityComponent.kt$KiwixActivityComponent$KiwixActivityComponent</ID>
|
||||
<ID>TooManyFunctions:KiwixMainActivity.kt$KiwixMainActivity$KiwixMainActivity</ID>
|
||||
<ID>TooManyFunctions:LibraryFragment.kt$LibraryFragment$LibraryFragment</ID>
|
||||
<ID>TooManyFunctions:WifiDirectManager.kt$WifiDirectManager$WifiDirectManager</ID>
|
||||
<ID>TooManyFunctions:ZimFileSelectFragment.kt$ZimFileSelectFragment$ZimFileSelectFragment</ID>
|
||||
<ID>TooManyFunctions:ZimHostActivity.kt$ZimHostActivity$ZimHostActivity</ID>
|
||||
<ID>TooManyFunctions:ZimManageViewModel.kt$ZimManageViewModel$ZimManageViewModel</ID>
|
||||
|
@ -1,482 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2019 Kiwix <android.kiwix.org>
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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<FileItem> 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<FileItem> 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<Unit>() {
|
||||
@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<FileItem> getFilesForTransfer() {
|
||||
return filesForTransfer;
|
||||
}
|
||||
|
||||
public void setFilesForTransfer(@NonNull ArrayList<FileItem> 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<FileItem> filesForTransfer);
|
||||
|
||||
void onFileStatusChanged(int itemIndex);
|
||||
|
||||
void onFileTransferComplete();
|
||||
}
|
||||
}
|
@ -0,0 +1,389 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (c) 2019 Kiwix <android.kiwix.org>
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
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<FileItem>
|
||||
|
||||
// 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<FileItem>) {
|
||||
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<FileItem>) {
|
||||
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<FileItem>)
|
||||
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('/')
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user