package io.mrarm.irc; import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.preference.PreferenceManager; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.documentfile.provider.DocumentFile; import androidx.appcompat.app.AlertDialog; import android.util.Log; import android.webkit.MimeTypeMap; import android.widget.Toast; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.nio.channels.FileChannel; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import io.mrarm.chatlib.irc.MessagePrefix; import io.mrarm.chatlib.irc.ServerConnectionApi; import io.mrarm.chatlib.irc.ServerConnectionData; import io.mrarm.chatlib.irc.dcc.DCCClient; import io.mrarm.chatlib.irc.dcc.DCCClientManager; import io.mrarm.chatlib.irc.dcc.DCCReverseClient; import io.mrarm.chatlib.irc.dcc.DCCServer; import io.mrarm.chatlib.irc.dcc.DCCServerManager; import io.mrarm.chatlib.irc.dcc.DCCUtils; import io.mrarm.irc.upnp.PortMapper; import io.mrarm.irc.upnp.rpc.AddPortMappingCall; import io.mrarm.irc.util.FormatUtils; public class DCCManager implements DCCServerManager.UploadListener, DCCClient.CloseListener, DCCReverseClient.StateListener { private static DCCManager sInstance; private static final String PREF_DCC_ASKED_FOR_PERMISSION = "dcc_storage_permission_asked"; private static final String PREF_DCC_ALWAYS_USE_APP_DOWNLOAD_DIR = "dcc_force_application_download_directory"; private static final String PREF_DCC_DIRECTORY_OVERRIDE_URI = "dcc_download_directory_uri"; private static final String PREF_DCC_DIRECTORY_OVERRIDE_URI_SYSTEM = "dcc_download_directory_uri_system"; public static DCCManager getInstance(Context context) { if (sInstance == null) sInstance = new DCCManager(context.getApplicationContext()); return sInstance; } private final Context mContext; private final SharedPreferences mPreferences; private final DCCServerManager mServer; private final DCCHistory mHistory; private final Map<DCCServer, DCCServerManager.UploadEntry> mUploads = new HashMap<>(); private final Map<DCCServerManager.UploadEntry, UploadServerInfo> mUploadServers = new HashMap<>(); private final Map<DCCServerManager.UploadEntry, PortMapper.PortMappingResult> mUploadPortMappings = new HashMap<>(); private final List<DCCServer.UploadSession> mSessions = new ArrayList<>(); private final List<DownloadInfo> mDownloads = new ArrayList<>(); private final List<DownloadListener> mDownloadListeners = new ArrayList<>(); private File mDownloadDirectory; private Uri mDownloadDirectoryOverrideURI; private boolean mIsDownloadDirectoryOverrideURISystem; private final File mFallbackDownloadDirectory; private boolean mAlwaysUseFallbackDir; private boolean mHasSystemDirectoryAccess; private final DCCNotificationManager mNotificationManager; private final Handler mHandler = new Handler(Looper.getMainLooper()); public DCCManager(Context context) { mContext = context; mPreferences = PreferenceManager.getDefaultSharedPreferences(context); mNotificationManager = new DCCNotificationManager(mContext); mFallbackDownloadDirectory = mContext.getExternalFilesDir("downloads"); mDownloadDirectory = mFallbackDownloadDirectory; mHistory = new DCCHistory(context); mServer = new DCCServerManager(); mServer.addUploadListener(this); mServer.addUploadListener(mNotificationManager); addDownloadListener(mNotificationManager); mAlwaysUseFallbackDir = mPreferences.getBoolean(PREF_DCC_ALWAYS_USE_APP_DOWNLOAD_DIR, false); String uri = mPreferences.getString(PREF_DCC_DIRECTORY_OVERRIDE_URI, null); if (uri != null) { mDownloadDirectoryOverrideURI = Uri.parse(uri); mIsDownloadDirectoryOverrideURISystem = mPreferences.getBoolean( PREF_DCC_DIRECTORY_OVERRIDE_URI_SYSTEM, false); } checkSystemDownloadsDirectoryAccess(); } public void setAlwaysUseApplicationDownloadDirectory(boolean value) { mAlwaysUseFallbackDir = value; mPreferences.edit() .putBoolean(PREF_DCC_ALWAYS_USE_APP_DOWNLOAD_DIR, value) .apply(); checkSystemDownloadsDirectoryAccess(); } public void setOverrideDownloadDirectory(Uri uri, boolean isSystem) { mDownloadDirectoryOverrideURI = uri; mIsDownloadDirectoryOverrideURISystem = isSystem; mPreferences.edit() .putString(PREF_DCC_DIRECTORY_OVERRIDE_URI, uri.toString()) .putBoolean(PREF_DCC_DIRECTORY_OVERRIDE_URI_SYSTEM, isSystem) .apply(); } public Uri getDownloadDirectoryOverrideURI() { if (mAlwaysUseFallbackDir) return null; return mDownloadDirectoryOverrideURI; } public boolean isDownloadDirectoryOverrideURISystem() { return mIsDownloadDirectoryOverrideURISystem; } public boolean isSystemDownloadDirectoryUsed() { return mHasSystemDirectoryAccess && (mDownloadDirectoryOverrideURI == null || mIsDownloadDirectoryOverrideURISystem); } private void checkSystemDownloadsDirectoryAccess() { if (mDownloadDirectoryOverrideURI != null && !mAlwaysUseFallbackDir) { DocumentFile dir = DocumentFile.fromTreeUri(mContext, mDownloadDirectoryOverrideURI); mHasSystemDirectoryAccess = dir.exists() && dir.canWrite(); return; } File downloadsDir = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS); if (downloadsDir != null && downloadsDir.canWrite() && !mAlwaysUseFallbackDir) { mDownloadDirectory = downloadsDir; mHasSystemDirectoryAccess = true; } else { mDownloadDirectory = mFallbackDownloadDirectory; mHasSystemDirectoryAccess = false; } Log.d("DCCManager", "Download directory: " + (mDownloadDirectory != null ? mDownloadDirectory.getAbsolutePath() : "null")); } public boolean needsAskSystemDownloadsPermission() { if (!mHasSystemDirectoryAccess) checkSystemDownloadsDirectoryAccess(); return !mHasSystemDirectoryAccess && !mPreferences.getBoolean(PREF_DCC_ASKED_FOR_PERMISSION, false) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; } public DCCNotificationManager getNotificationManager() { return mNotificationManager; } public DCCServerManager getServer() { return mServer; } public DCCHistory getHistory() { return mHistory; } public DCCClientManager createClient(ServerConnectionInfo server) { return new ClientImpl(server); } public void addDownloadListener(DownloadListener listener) { synchronized (mDownloads) { mDownloadListeners.add(listener); } } public void removeDownloadListener(DownloadListener listener) { synchronized (mDownloads) { mDownloadListeners.remove(listener); } } @Override public void onUploadCreated(DCCServerManager.UploadEntry uploadEntry) { synchronized (mUploads) { mUploads.put(uploadEntry.getServer(), uploadEntry); ServerConnectionData connection = uploadEntry.getConnection(); ServerConnectionInfo connectionInfo = null; for (ServerConnectionInfo info : ServerConnectionManager.getInstance(mContext) .getConnections()) { if (((ServerConnectionApi) info.getApiInstance()).getServerConnectionData() == connection) { connectionInfo = info; break; } } mUploadServers.put(uploadEntry, connectionInfo != null ? new UploadServerInfo(connectionInfo) : null); } } @Override public void onUploadDestroyed(DCCServerManager.UploadEntry uploadEntry) { synchronized (mUploads) { mUploads.remove(uploadEntry.getServer()); mUploadServers.remove(uploadEntry); PortMapper.PortMappingResult mapping; if ((mapping = mUploadPortMappings.remove(uploadEntry)) != null) { if (Thread.currentThread() == Looper.getMainLooper().getThread()) AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> deleteUploadPortMapping(mapping)); else deleteUploadPortMapping(mapping); } } } private void deleteUploadPortMapping(PortMapper.PortMappingResult mapping) { try { PortMapper.removePortMapping(mapping); } catch (Exception e) { Log.w("DCCManager", "Failed to remove port mapping for port " + mapping.getExternalPort()); e.printStackTrace(); } } @Override public void onSessionCreated(DCCServer dccServer, DCCServer.UploadSession uploadSession) { synchronized (mSessions) { mSessions.add(uploadSession); } } @Override public void onSessionDestroyed(DCCServer dccServer, DCCServer.UploadSession uploadSession) { DCCServerManager.UploadEntry entry; UploadServerInfo uploadServerInfo; boolean shouldClose; synchronized (mSessions) { mSessions.remove(uploadSession); shouldClose = uploadSession.getAcknowledgedSize() >= uploadSession.getTotalSize(); if (shouldClose) { for (DCCServer.UploadSession s : mSessions) { if (s.getServer() == dccServer) { shouldClose = false; break; } } } } synchronized (mUploads) { entry = mUploads.get(dccServer); uploadServerInfo = mUploadServers.get(entry); } if (shouldClose && entry != null) mHandler.post(() -> mServer.cancelUpload(entry)); if (entry != null && uploadServerInfo != null) mHistory.addEntry(new DCCHistory.Entry(entry, uploadSession, uploadServerInfo, new Date())); } public void onDownloadCreated(DownloadInfo download) { synchronized (mDownloads) { mDownloads.add(download); for (DownloadListener listener : mDownloadListeners) listener.onDownloadCreated(download); } } public void onDownloadDestroyed(DownloadInfo download) { synchronized (mDownloads) { mDownloads.remove(download); for (DownloadListener listener : mDownloadListeners) listener.onDownloadDestroyed(download); } mHistory.addEntry(new DCCHistory.Entry(download, new Date())); } @Override public void onClosed(DCCClient dccClient) { DownloadInfo download = null; synchronized (mDownloads) { for (int i = mDownloads.size() - 1; i >= 0; --i) { download = mDownloads.get(i); if (download.getClient() == dccClient) { mDownloads.remove(i); for (DownloadListener listener : mDownloadListeners) listener.onDownloadDestroyed(download); break; } } } if (download != null) mHistory.addEntry(new DCCHistory.Entry(download, new Date())); } @Override public void onClosed(DCCReverseClient dccReverseClient) { DownloadInfo download = null; synchronized (mDownloads) { for (int i = mDownloads.size() - 1; i >= 0; --i) { download = mDownloads.get(i); if (download.getReverseClient() == dccReverseClient) { mDownloads.remove(i); for (DownloadListener listener : mDownloadListeners) listener.onDownloadDestroyed(download); break; } } } if (download != null) mHistory.addEntry(new DCCHistory.Entry(download, new Date())); } @Override public void onClientConnected(DCCReverseClient dccReverseClient, DCCClient dccClient) { synchronized (mDownloads) { for (DownloadInfo download : mDownloads) { if (download.getReverseClient() == dccReverseClient) { for (DownloadListener listener : mDownloadListeners) listener.onDownloadUpdated(download); return; } } } } public DCCServerManager.UploadEntry getUploadEntry(DCCServer server) { synchronized (mUploads) { return mUploads.get(server); } } public UploadServerInfo getUploadServerInfo(DCCServerManager.UploadEntry upload) { synchronized (mUploads) { return mUploadServers.get(upload); } } public String getUploadName(DCCServer server) { synchronized (mUploads) { DCCServerManager.UploadEntry ent = mUploads.get(server); if (ent == null) return null; return ent.getFileName(); } } public List<DCCServerManager.UploadEntry> getUploads() { synchronized (mUploads) { return new ArrayList<>(mUploads.values()); } } public List<DCCServer.UploadSession> getUploadSessions() { synchronized (mSessions) { return new ArrayList<>(mSessions); } } public boolean hasAnyDownloads() { synchronized (mSessions) { return !mDownloads.isEmpty(); } } public boolean hasAnyActiveDownloads() { synchronized (mSessions) { for (DownloadInfo download : mDownloads) { if (!download.isPending()) return true; } return false; } } public List<DownloadInfo> getDownloads() { synchronized (mDownloads) { return new ArrayList<>(mDownloads); } } public void startUpload(ServerConnectionInfo server, String channel, DCCServer.FileChannelFactory file, String fileName, long fileSize) { ServerConnectionData connectionData = ((ServerConnectionApi) server.getApiInstance()) .getServerConnectionData(); if (ServerConnectionManager.isWifiConnected(mContext)) { AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { DCCServerManager.UploadEntry upload = null; PortMapper.PortMappingResult mapping = null; try { upload = mServer.startUpload(connectionData, channel, fileName, file); mapping = PortMapper.mapPort(new PortMapper.PortMappingRequest( AddPortMappingCall.PROTOCOL_TCP, upload.getPort(), upload.getPort(), "Revolution IRC DCC transfer")); synchronized (mUploads) { if (!mUploads.containsKey(upload.getServer())) throw new IOException("Upload cancelled while we were setting up" + " port mapping"); mUploadPortMappings.put(upload, mapping); } mServer.setUploadPortForwarded(upload, mapping.getExternalPort()); server.getApiInstance().sendMessage(channel, DCCUtils.buildSendMessage( mapping.getExternalIP(), fileName, mapping.getExternalPort(), fileSize), null, null); } catch (IOException e) { e.printStackTrace(); mHandler.post(() -> Toast .makeText(mContext, R.string.error_generic, Toast.LENGTH_SHORT).show()); if (upload != null) mServer.cancelUpload(upload); if (mapping != null) { try { PortMapper.removePortMapping(mapping); } catch (Exception e2) { Log.w("DCCManager", "Failed to remove port mapping " + "in error handler"); e2.printStackTrace(); } } // fall back to reverse DCC upload = mServer.addReverseUpload(connectionData, channel, fileName, file); server.getApiInstance().sendMessage(channel, DCCUtils.buildSendMessage( "127.0.0.1", fileName, 0, fileSize, upload.getReverseId()), null, null); } }); } else { // fall back to reverse DCC DCCServerManager.UploadEntry upload = mServer.addReverseUpload( connectionData, channel, fileName, file); server.getApiInstance().sendMessage(channel, DCCUtils.buildSendMessage( "0.0.0.0", fileName, 0, fileSize, upload.getReverseId()), null, null); } } public class UploadServerInfo { private final UUID mServerUUID; private final String mServerName; public UploadServerInfo(UUID uuid, String serverName) { this.mServerUUID = uuid; this.mServerName = serverName; } public UploadServerInfo(ServerConnectionInfo connectionInfo) { this.mServerUUID = connectionInfo.getUUID(); this.mServerName = connectionInfo.getName(); } public UUID getServerUUID() { return mServerUUID; } public String getServerName() { return mServerName; } } public class DownloadInfo { private final UUID mServerUUID; private final String mServerName; private final MessagePrefix mSender; private final String mFileName; private final long mFileSize; private final String mAddress; private final int mPort; private final int mReverseUploadId; private boolean mPending = true; private boolean mCancelled = false; private DCCClient mClient; private DCCReverseClient mReverseClient; private Uri mDownloadedTo; private DownloadInfo(ServerConnectionInfo server, MessagePrefix sender, String fileName, long fileSize, String address, int port) { mServerUUID = server.getUUID(); mServerName = server.getName(); mSender = sender; mFileName = fileName; mFileSize = fileSize; mAddress = address; mPort = port; mReverseUploadId = -1; } private DownloadInfo(ServerConnectionInfo server, MessagePrefix sender, String fileName, long fileSize, int reverseUploadId) { mServerUUID = server.getUUID(); mServerName = server.getName(); mSender = sender; mFileName = fileName; mFileSize = fileSize; mAddress = null; mPort = -1; mReverseUploadId = reverseUploadId; } public String getServerName() { return mServerName; } public UUID getServerUUID() { return mServerUUID; } public MessagePrefix getSender() { return mSender; } public String getRawFileName() { return mFileName; } public String getUnescapedFileName() { return DCCUtils.unescapeFilename(mFileName); } private String getFileExtension() { String fileName = getUnescapedFileName(); int iof = fileName.lastIndexOf('.'); if (iof == -1) return null; return fileName.substring(iof + 1); } public long getFileSize() { return mFileSize; } public synchronized Uri getDownloadedTo() { return mDownloadedTo; } public boolean isPending() { return mPending; } public boolean isReverse() { return mReverseUploadId != -1; } public synchronized DCCClient getClient() { if (mReverseClient != null) return mReverseClient.getClient(); return mClient; } public synchronized DCCReverseClient getReverseClient() { return mReverseClient; } private void createClient() throws IOException { ServerConnectionInfo connection = ServerConnectionManager.getInstance(mContext) .getConnection(mServerUUID); if (connection == null) throw new IOException("The connection doesn't exist"); FileChannel file; String downloadFileName = getUnescapedFileName().replace('/', '_'); String ext = getFileExtension(); if (mDownloadDirectoryOverrideURI != null && !mAlwaysUseFallbackDir) { DocumentFile dir = DocumentFile.fromTreeUri(mContext, mDownloadDirectoryOverrideURI); String mime = ext != null ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) : null; if (mime == null) mime = "application/octet-stream"; DocumentFile docFile = dir.createFile(mime, downloadFileName); OutputStream stream = mContext.getContentResolver().openOutputStream( docFile.getUri()); if (!(stream instanceof FileOutputStream)) throw new IOException("stream is not a file"); file = ((FileOutputStream) stream).getChannel(); synchronized (this) { mDownloadedTo = docFile.getUri(); } Log.d("DCCManager", "Starting a download: " + docFile.getUri().toString()); } else { if (mDownloadDirectory == null) throw new IOException("Download directory is null"); File filePath = new File(mDownloadDirectory, downloadFileName); int attempt = 1; while (filePath.exists()) { filePath = new File(mDownloadDirectory, (ext != null ? downloadFileName.substring(0, downloadFileName.length() - ext.length() - 1) : downloadFileName) + " (" + attempt + ")" + (ext != null ? "." + ext : "")); attempt++; } file = new FileOutputStream(filePath).getChannel(); synchronized (this) { mDownloadedTo = Uri.fromFile(filePath); } Log.d("DCCManager", "Starting a download: " + filePath.getAbsolutePath()); } try { if (isReverse()) { synchronized (this) { if (mCancelled) throw new CancelledException(); mReverseClient = new DCCReverseClient(file, 0L, mFileSize); } mReverseClient.setStateListener(DCCManager.this); int port = mReverseClient.createServerSocket(); String message = DCCUtils.buildSendMessage(getLocalIP(), mFileName, port, mFileSize, mReverseUploadId); connection.getApiInstance().sendMessage(mSender.getNick(), message, null, null); } else { SocketChannel socket = SocketChannel.open( new InetSocketAddress(mAddress, mPort)); synchronized (this) { if (mCancelled) throw new CancelledException(); mClient = new DCCClient(file, 0L, mFileSize); } mClient.setCloseListener(DCCManager.this); mClient.start(socket); } } catch (Exception e) { try { file.close(); } catch (IOException ignored) { } synchronized (this) { if (mClient != null) mClient.close(); mClient = null; if (mReverseClient != null) mReverseClient.close(); mReverseClient = null; } throw e; } } public void approve() { if (!mPending || mCancelled) return; mPending = false; AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { try { createClient(); } catch (CancelledException e) { onDownloadDestroyed(this); return; } catch (IOException e) { mHandler.post(() -> Toast.makeText(mContext, R.string.error_generic, Toast.LENGTH_SHORT) .show()); e.printStackTrace(); onDownloadDestroyed(this); return; } synchronized (mDownloads) { for (DownloadListener listener : mDownloadListeners) listener.onDownloadUpdated(this); } }); } public void reject() { if (!mPending || mCancelled) return; onDownloadDestroyed(this); } public void cancel() { if (mPending) { onDownloadDestroyed(this); } else { synchronized (this) { mCancelled = true; if (mClient != null) { mClient.close(); mClient = null; } if (mReverseClient != null) { mReverseClient.close(); mReverseClient = null; } } } } public AlertDialog createDownloadApprovalDialog(Context context, ActivityDialogHandler handler) { String title; if (getFileSize() > 0) title = context.getString(R.string.dcc_approve_download_title_with_size, getUnescapedFileName(), FormatUtils.formatByteSize(getFileSize())); else title = context.getString(R.string.dcc_approve_download_title, getUnescapedFileName()); AlertDialog ret = new AlertDialog.Builder(context) .setTitle(title) .setMessage(context.getString(R.string.dcc_approve_download_body, mSender.toString(), getServerName())) .setPositiveButton(R.string.action_accept, (DialogInterface dialog, int which) -> { if (needsAskSystemDownloadsPermission()) handler.askSystemDownloadsPermission(() -> approve()); else approve(); }) .setNegativeButton(R.string.action_reject, (DialogInterface dialog, int which) -> reject()) .setOnCancelListener((DialogInterface dialog) -> reject()) .create(); ret.setCanceledOnTouchOutside(false); return ret; } } private static class CancelledException extends IOException { public CancelledException() { super(); } } public static class ActivityDialogHandler implements DownloadListener { private Activity mActivity; private AlertDialog mCurrentDialog; private int mStoragePermissionRequestCode; private int mDownloadsPermissionRequestCode; private List<Runnable> mStoragePermissionRequestCallbacks; private boolean mPermissionRequestPending; public ActivityDialogHandler(Activity activity, int storagePermissionRequestCode, int downloadsPermissionRequestCode) { mActivity = activity; mStoragePermissionRequestCode = storagePermissionRequestCode; mDownloadsPermissionRequestCode = downloadsPermissionRequestCode; } public void onResume() { DCCManager.getInstance(mActivity).addDownloadListener(this); showDialogsIfNeeded(); } public void onPause() { DCCManager.getInstance(mActivity).removeDownloadListener(this); if (mCurrentDialog != null) { mCurrentDialog.dismiss(); mCurrentDialog = null; } } private void showDialog(AlertDialog dialog) { if (mCurrentDialog != null) mCurrentDialog.dismiss(); mCurrentDialog = dialog; dialog.setOnDismissListener((DialogInterface i) -> { mCurrentDialog = null; showDialogsIfNeeded(); }); dialog.show(); } private void showDialogsIfNeeded() { if (mCurrentDialog != null || mPermissionRequestPending) return; for (DownloadInfo download : DCCManager.getInstance(mActivity).getDownloads()) { if (download.isPending()) showDialog(download.createDownloadApprovalDialog(mActivity, this)); } } @Override public void onDownloadCreated(DownloadInfo download) { if (download.isPending()) { mActivity.runOnUiThread(() -> { if (mCurrentDialog == null && download.isPending()) // download is still pending showDialog(download.createDownloadApprovalDialog(mActivity, this)); }); } } @Override public void onDownloadDestroyed(DownloadInfo download) { } @Override public void onDownloadUpdated(DownloadInfo download) { } protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == mDownloadsPermissionRequestCode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (resultCode == Activity.RESULT_OK) { mActivity.getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); DCCManager.getInstance(mActivity).setOverrideDownloadDirectory( data.getData(), true); onSystemDownloadPermissionRequestFinished(); } else { showSystemDownloadsPermissionDenialDialog(); } } } public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == mStoragePermissionRequestCode) { String p = Manifest.permission.WRITE_EXTERNAL_STORAGE; if (ContextCompat.checkSelfPermission(mActivity, p) != PackageManager.PERMISSION_GRANTED && ActivityCompat.shouldShowRequestPermissionRationale(mActivity, p)) { showSystemDownloadsPermissionDenialDialog(); } else { onSystemDownloadPermissionRequestFinished(); } } } private void askSystemDownloadsPermission(Runnable cb, boolean noShowDenialDialog) { if (cb != null) { if (mStoragePermissionRequestCallbacks == null) mStoragePermissionRequestCallbacks = new ArrayList<>(); mStoragePermissionRequestCallbacks.add(cb); } mPermissionRequestPending = true; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { StorageManager manager = (StorageManager) mActivity .getSystemService(Context.STORAGE_SERVICE); StorageVolume volume = manager.getPrimaryStorageVolume(); Intent intent = volume.createAccessIntent(Environment.DIRECTORY_DOWNLOADS); intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); mActivity.startActivityForResult(intent, mDownloadsPermissionRequestCode); } else { if (ActivityCompat.shouldShowRequestPermissionRationale(mActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE) && !noShowDenialDialog) { showSystemDownloadsPermissionDenialDialog(); } else { ActivityCompat.requestPermissions(mActivity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, mStoragePermissionRequestCode); } } } public void askSystemDownloadsPermission(Runnable cb) { askSystemDownloadsPermission(cb, false); } private void onSystemDownloadPermissionRequestFinished() { if (mStoragePermissionRequestCallbacks != null) { DCCManager.getInstance(mActivity).checkSystemDownloadsDirectoryAccess(); for (Runnable r : mStoragePermissionRequestCallbacks) r.run(); } } private void showSystemDownloadsPermissionDenialDialog() { new AlertDialog.Builder(mActivity) .setTitle(R.string.dcc_system_downloads_permission_dialog_title) .setMessage(R.string.dcc_system_downloads_permission_dialog_text) .setPositiveButton(R.string.action_ok, (DialogInterface i, int w) -> { DCCManager.getInstance(mActivity).mPreferences.edit() .putBoolean(PREF_DCC_ASKED_FOR_PERMISSION, true) .apply(); onSystemDownloadPermissionRequestFinished(); }) .setNegativeButton(R.string.action_ask_again, (DialogInterface i, int w) -> { askSystemDownloadsPermission(null, true); }) .show(); } } private class ClientImpl extends DCCClientManager { private ServerConnectionInfo mServer; public ClientImpl(ServerConnectionInfo server) { mServer = server; } @Override public void onFileOffered(ServerConnectionData connection, MessagePrefix sender, String fileName, String address, int port, long fileSize) { Log.d("DCCManager", "File offered: " + fileName + " from " + address + ":" + port); onDownloadCreated(new DownloadInfo(mServer, sender, fileName, fileSize, address, port)); } @Override public void onFileOfferedUsingReverse(ServerConnectionData connection, MessagePrefix sender, String fileName, long fileSize, int uploadId) { Log.d("DCCManager", "File offered: " + fileName + " (reverse)"); onDownloadCreated(new DownloadInfo(mServer, sender, fileName, fileSize, uploadId)); } } public interface DownloadListener { void onDownloadCreated(DownloadInfo download); void onDownloadDestroyed(DownloadInfo download); void onDownloadUpdated(DownloadInfo download); } public static String getLocalIP() { try { Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { NetworkInterface iface = interfaces.nextElement(); Enumeration<InetAddress> addrs = iface.getInetAddresses(); while (addrs.hasMoreElements()) { InetAddress addr = addrs.nextElement(); if (addr.isLoopbackAddress()) continue; String hostAddr = addr.getHostAddress(); if (hostAddr.indexOf(':') != -1) { // IPv6 continue; /* int iof = hostAddr.indexOf('%'); if (iof != -1) hostAddr = hostAddr.substring(0, iof); */ } return hostAddr; } } } catch (SocketException ignored) { } return null; } }