Merge pull request #28 from rthakohov/network-browsing-broadcast

Network browsing via broadcasting
This commit is contained in:
Ruslan Tkhakokhov 2017-07-27 15:50:37 -07:00 committed by GitHub
commit baf3ba19f4
6 changed files with 430 additions and 108 deletions

View file

@ -17,8 +17,12 @@
package com.google.android.sambadocumentsprovider.browsing; package com.google.android.sambadocumentsprovider.browsing;
class BrowsingException extends Exception { public class BrowsingException extends Exception {
BrowsingException(String message) { public BrowsingException(String message) {
super("Browsing failed: " + message); super("Browsing failed: " + message);
} }
public BrowsingException(String message, Throwable cause) {
super("Browsing failed: " + message, cause);
}
} }

View file

@ -22,30 +22,24 @@ import android.os.AsyncTask;
import android.util.Log; import android.util.Log;
import com.google.android.sambadocumentsprovider.TaskManager; import com.google.android.sambadocumentsprovider.TaskManager;
import com.google.android.sambadocumentsprovider.base.DirectoryEntry;
import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback; import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback;
import com.google.android.sambadocumentsprovider.browsing.broadcast.BroadcastBrowsingProvider;
import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; import com.google.android.sambadocumentsprovider.nativefacade.SmbClient;
import com.google.android.sambadocumentsprovider.nativefacade.SmbDir;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
public class NetworkBrowser { public class NetworkBrowser {
public static final Uri SMB_BROWSING_URI = Uri.parse("smb://"); public static final Uri SMB_BROWSING_URI = Uri.parse("smb://");
private static final String TAG = "NetworkBrowser"; private static final String TAG = "NetworkBrowser";
private final NetworkBrowsingProvider mMasterProvider;
private final TaskManager mTaskManager;
private final Map<Uri, Future> mTasks = new HashMap<>(); private final NetworkBrowsingProvider mMasterProvider;
private final NetworkBrowsingProvider mBroadcastProvider;
private final TaskManager mTaskManager;
public NetworkBrowser(SmbClient client, TaskManager taskManager) { public NetworkBrowser(SmbClient client, TaskManager taskManager) {
mMasterProvider = new MasterBrowsingProvider(client); mMasterProvider = new MasterBrowsingProvider(client);
mBroadcastProvider = new BroadcastBrowsingProvider();
mTaskManager = taskManager; mTaskManager = taskManager;
} }
@ -58,7 +52,19 @@ public class NetworkBrowser {
} }
private List<String> getServers() throws BrowsingException { private List<String> getServers() throws BrowsingException {
return mMasterProvider.getServers(); List<String> servers = null;
try {
servers = mMasterProvider.getServers();
} catch (BrowsingException e) {
Log.e(TAG, "Master browsing failed", e);
}
if (servers == null || servers.isEmpty()) {
return mBroadcastProvider.getServers();
}
return servers;
} }
private class LoadServersTask extends AsyncTask<Void, Void, List<String>> { private class LoadServersTask extends AsyncTask<Void, Void, List<String>> {

View file

@ -19,7 +19,7 @@ package com.google.android.sambadocumentsprovider.browsing;
import java.util.List; import java.util.List;
interface NetworkBrowsingProvider { public interface NetworkBrowsingProvider {
/** /**
* Returns unresolved host names. * Returns unresolved host names.
*/ */

View file

@ -0,0 +1,129 @@
/*
* Copyright 2017 Google Inc.
*
* 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 com.google.android.sambadocumentsprovider.browsing.broadcast;
import android.util.Log;
import com.google.android.sambadocumentsprovider.BuildConfig;
import com.google.android.sambadocumentsprovider.browsing.BrowsingException;
import com.google.android.sambadocumentsprovider.browsing.NetworkBrowsingProvider;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class BroadcastBrowsingProvider implements NetworkBrowsingProvider {
private static final String TAG = "BroadcastBrowsing";
private static final int LISTENING_TIMEOUT = 3000; // 3 seconds.
private static final int NBT_PORT = 137;
private final ExecutorService mExecutor = Executors.newCachedThreadPool();
private int mTransId = 0;
@Override
public List<String> getServers() throws BrowsingException {
try {
List<Future<List<String>>> serverFutures = new ArrayList<>();
List<String> broadcastAddresses = BroadcastUtils.getBroadcastAddress();
for (String address : broadcastAddresses) {
mTransId++;
serverFutures.add(mExecutor.submit(new GetServersForInterfaceTask(address, mTransId)));
}
List<String> servers = new ArrayList<>();
for (Future<List<String>> future : serverFutures) {
servers.addAll(future.get());
}
return servers;
} catch (IOException | ExecutionException | InterruptedException e) {
Log.e(TAG, "Failed to get servers via broadcast", e);
throw new BrowsingException("Failed to get servers via broadcast", e);
}
}
private class GetServersForInterfaceTask implements Callable<List<String>> {
private final String mBroadcastAddress;
private final int mTransId;
GetServersForInterfaceTask(String broadcastAddress, int transId) {
mBroadcastAddress = broadcastAddress;
mTransId = transId;
}
@Override
public List<String> call() throws Exception {
try (DatagramSocket socket = new DatagramSocket()) {
InetAddress address = InetAddress.getByName(mBroadcastAddress);
sendNameQueryBroadcast(socket, address);
return listenForServers(socket);
}
}
private void sendNameQueryBroadcast(
DatagramSocket socket,
InetAddress address) throws IOException {
byte[] data = BroadcastUtils.createPacket(mTransId);
int dataLength = data.length;
DatagramPacket packet = new DatagramPacket(data, 0, dataLength, address, NBT_PORT);
socket.send(packet);
if (BuildConfig.DEBUG) Log.d(TAG, "Broadcast package sent");
}
private List<String> listenForServers(DatagramSocket socket) throws IOException {
List<String> servers = new ArrayList<>();
socket.setSoTimeout(LISTENING_TIMEOUT);
while (true) {
try {
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
try {
servers.addAll(BroadcastUtils.extractServers(packet.getData(), mTransId));
} catch (BrowsingException e) {
Log.e(TAG, "Failed to parse incoming packet: ", e);
}
} catch (SocketTimeoutException e) {
break;
}
}
return servers;
}
}
}

View file

@ -0,0 +1,183 @@
/*
* Copyright 2017 Google Inc.
*
* 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 com.google.android.sambadocumentsprovider.browsing.broadcast;
import android.util.Log;
import com.google.android.sambadocumentsprovider.BuildConfig;
import com.google.android.sambadocumentsprovider.browsing.BrowsingException;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
class BroadcastUtils {
private static final String TAG = "BroadcastUtils";
private static final int FILE_SERVER_NODE_TYPE = 0x20;
private static final int SERVER_NAME_LENGTH = 15;
private static final String SERVER_NAME_CHARSET = "US-ASCII";
/**
* Generates a NetBIOS name query request.
* https://tools.ietf.org/html/rfc1002
* Section 4.2.12
*/
static byte[] createPacket(int transId) {
ByteBuffer os = ByteBuffer.allocate(50);
char broadcastFlag = 0x0010;
char questionCount = 1;
char answerResourceCount = 0;
char authorityResourceCount = 0;
char additionalResourceCount = 0;
os.putChar((char) transId);
os.putChar(broadcastFlag);
os.putChar(questionCount);
os.putChar(answerResourceCount);
os.putChar(authorityResourceCount);
os.putChar(additionalResourceCount);
// Length of name. 16 bytes of name encoded to 32 bytes.
os.put((byte) 0x20);
// '*' character encodes to 2 bytes.
os.put((byte) 0x43);
os.put((byte) 0x4b);
// Write the remaining 15 nulls which encode to 30* 0x41
for (int i = 0; i < 30; i++) {
os.put((byte) 0x41);
}
// Length of next segment.
os.put((byte) 0);
// Question type: Node status
os.putChar((char) 0x21);
// Question class: Internet
os.putChar((char) 0x01);
return os.array();
}
/**
* Parses a positive response to NetBIOS name request query.
* https://tools.ietf.org/html/rfc1002
* Section 4.2.13
*/
static List<String> extractServers(byte[] data, int expectedTransId) throws BrowsingException {
try {
ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
int transId = buffer.getChar();
if (transId != expectedTransId) {
// This response is not to our broadcast.
if (BuildConfig.DEBUG) Log.d(TAG, "Irrelevant broadcast response");
return Collections.emptyList();
}
skipBytes(buffer, 2); // Skip flags.
skipBytes(buffer, 2); // No questions.
skipBytes(buffer, 2); // Skip answers count.
skipBytes(buffer, 2); // No authority resources.
skipBytes(buffer, 2); // No additional resources.
int nameLength = buffer.get();
skipBytes(buffer, nameLength);
skipBytes(buffer, 1);
int nodeStatus = buffer.getChar();
if (nodeStatus != 0x20 && nodeStatus != 0x21) {
throw new BrowsingException("Received negative response for the broadcast");
}
skipBytes(buffer, 2);
skipBytes(buffer, 4);
skipBytes(buffer, 2);
int addressListEntryCount = buffer.get();
List<String> servers = new ArrayList<>();
for (int i = 0; i < addressListEntryCount; i++) {
byte[] nameArray = new byte[SERVER_NAME_LENGTH];
buffer.get(nameArray, 0, SERVER_NAME_LENGTH);
final String serverName = new String(nameArray, Charset.forName(SERVER_NAME_CHARSET));
final int type = buffer.get();
if (type == FILE_SERVER_NODE_TYPE) {
servers.add(serverName);
}
skipBytes(buffer, 2);
}
return servers;
} catch (BufferUnderflowException e) {
Log.e(TAG, "Malformed incoming packet");
return Collections.emptyList();
}
}
static List<String> getBroadcastAddress() throws BrowsingException, SocketException {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
List<String> broadcastAddresses = new ArrayList<>();
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = interfaces.nextElement();
if (networkInterface.isLoopback()) {
continue;
}
for (InterfaceAddress interfaceAddress :
networkInterface.getInterfaceAddresses()) {
InetAddress broadcast = interfaceAddress.getBroadcast();
if (broadcast != null) {
broadcastAddresses.add(broadcast.toString().substring(1));
}
}
}
return broadcastAddresses;
}
private static void skipBytes(ByteBuffer buffer, int bytes) {
for (int i = 0; i < bytes; i++) {
buffer.get();
}
}
}

View file

@ -75,75 +75,75 @@ public class SambaDocumentsProvider extends DocumentsProvider {
private static final String TAG = "SambaDocumentsProvider"; private static final String TAG = "SambaDocumentsProvider";
private static final String[] DEFAULT_ROOT_PROJECTION = { private static final String[] DEFAULT_ROOT_PROJECTION = {
Root.COLUMN_ROOT_ID, Root.COLUMN_ROOT_ID,
Root.COLUMN_DOCUMENT_ID, Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_TITLE, Root.COLUMN_TITLE,
Root.COLUMN_FLAGS, Root.COLUMN_FLAGS,
Root.COLUMN_ICON Root.COLUMN_ICON
}; };
private static final String[] DEFAULT_DOCUMENT_PROJECTION = { private static final String[] DEFAULT_DOCUMENT_PROJECTION = {
Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME, Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_FLAGS, Document.COLUMN_FLAGS,
Document.COLUMN_MIME_TYPE, Document.COLUMN_MIME_TYPE,
Document.COLUMN_SIZE, Document.COLUMN_SIZE,
Document.COLUMN_LAST_MODIFIED, Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_ICON Document.COLUMN_ICON
}; };
private final OnTaskFinishedCallback<Uri> mLoadDocumentCallback = private final OnTaskFinishedCallback<Uri> mLoadDocumentCallback =
new OnTaskFinishedCallback<Uri>() { new OnTaskFinishedCallback<Uri>() {
@Override @Override
public void onTaskFinished(@Status int status, @Nullable Uri uri, Exception exception) { public void onTaskFinished(@Status int status, @Nullable Uri uri, Exception exception) {
getContext().getContentResolver().notifyChange(toNotifyUri(uri), null, false); getContext().getContentResolver().notifyChange(toNotifyUri(uri), null, false);
} }
}; };
private final OnTaskFinishedCallback<DocumentMetadata> mLoadChildrenCallback = private final OnTaskFinishedCallback<DocumentMetadata> mLoadChildrenCallback =
new OnTaskFinishedCallback<DocumentMetadata>() { new OnTaskFinishedCallback<DocumentMetadata>() {
@Override @Override
public void onTaskFinished(@Status int status, DocumentMetadata metadata, public void onTaskFinished(@Status int status, DocumentMetadata metadata,
Exception exception) { Exception exception) {
// Notify remote side that we get the list even though we don't have the stat yet. // Notify remote side that we get the list even though we don't have the stat yet.
// If it failed we still should notify the remote side that the loading failed. // If it failed we still should notify the remote side that the loading failed.
getContext().getContentResolver().notifyChange( getContext().getContentResolver().notifyChange(
toNotifyUri(metadata.getUri()), null, false); toNotifyUri(metadata.getUri()), null, false);
} }
}; };
private final OnTaskFinishedCallback<String> mWriteFinishedCallback = private final OnTaskFinishedCallback<String> mWriteFinishedCallback =
new OnTaskFinishedCallback<String>() { new OnTaskFinishedCallback<String>() {
@Override @Override
public void onTaskFinished( public void onTaskFinished(
@Status int status, @Nullable String item, Exception exception) { @Status int status, @Nullable String item, Exception exception) {
final Uri uri = toUri(item); final Uri uri = toUri(item);
try (final CacheResult result = mCache.get(uri)) { try (final CacheResult result = mCache.get(uri)) {
if (result.getState() != CacheResult.CACHE_MISS) { if (result.getState() != CacheResult.CACHE_MISS) {
result.getItem().reset(); result.getItem().reset();
} }
} }
final Uri parentUri = DocumentMetadata.buildParentUri(uri); final Uri parentUri = DocumentMetadata.buildParentUri(uri);
getContext().getContentResolver().notifyChange(toNotifyUri(parentUri), null, false); getContext().getContentResolver().notifyChange(toNotifyUri(parentUri), null, false);
} }
}; };
private final OnTaskFinishedCallback<List<String>> mLoadSharesFinishedCallback = private final OnTaskFinishedCallback<List<String>> mLoadSharesFinishedCallback =
new OnTaskFinishedCallback<List<String>>() { new OnTaskFinishedCallback<List<String>>() {
@Override @Override
public void onTaskFinished( public void onTaskFinished(
@OnTaskFinishedCallback.Status int status, @OnTaskFinishedCallback.Status int status,
@Nullable List<String> item, @Nullable List<String> item,
@Nullable Exception exception) { @Nullable Exception exception) {
if (BuildConfig.DEBUG) Log.d(TAG, "Browsing callback"); if (BuildConfig.DEBUG) Log.d(TAG, "Browsing callback");
mBrowsingStorage = item; mBrowsingStorage = item;
getContext().getContentResolver().notifyChange( getContext().getContentResolver().notifyChange(
toNotifyUri(toUri(NetworkBrowser.SMB_BROWSING_URI.toString())), null, false); toNotifyUri(toUri(NetworkBrowser.SMB_BROWSING_URI.toString())), null, false);
} }
}; };
private final MountedShareChangeListener mShareChangeListener = new MountedShareChangeListener() { private final MountedShareChangeListener mShareChangeListener = new MountedShareChangeListener() {
@Override @Override
@ -162,7 +162,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
private StorageManager mStorageManager; private StorageManager mStorageManager;
private NetworkBrowser mNetworkBrowser; private NetworkBrowser mNetworkBrowser;
private List<String> mBrowsingStorage = new ArrayList<>(); private List<String> mBrowsingStorage;
@Override @Override
public boolean onCreate() { public boolean onCreate() {
@ -189,11 +189,11 @@ public class SambaDocumentsProvider extends DocumentsProvider {
MatrixCursor cursor = new MatrixCursor(projection, mShareManager.size()); MatrixCursor cursor = new MatrixCursor(projection, mShareManager.size());
cursor.addRow(new Object[] { cursor.addRow(new Object[] {
NetworkBrowser.SMB_BROWSING_URI.toString(), NetworkBrowser.SMB_BROWSING_URI.toString(),
NetworkBrowser.SMB_BROWSING_URI.toString(), NetworkBrowser.SMB_BROWSING_URI.toString(),
getContext().getResources().getString(R.string.browsing_root_name), getContext().getResources().getString(R.string.browsing_root_name),
0, 0,
R.drawable.ic_cloud, R.drawable.ic_cloud,
}); });
for (String uri : mShareManager) { for (String uri : mShareManager) {
@ -211,11 +211,11 @@ public class SambaDocumentsProvider extends DocumentsProvider {
name = metadata.getDisplayName(); name = metadata.getDisplayName();
cursor.addRow(new Object[] { cursor.addRow(new Object[] {
toRootId(metadata), toRootId(metadata),
toDocumentId(parsedUri), toDocumentId(parsedUri),
name, name,
Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_IS_CHILD,
R.drawable.ic_folder_shared R.drawable.ic_folder_shared
}); });
} }
@ -273,7 +273,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
@Override @Override
public Cursor queryChildDocuments(String documentId, String[] projection, String sortOrder) public Cursor queryChildDocuments(String documentId, String[] projection, String sortOrder)
throws FileNotFoundException { throws FileNotFoundException {
if (BuildConfig.DEBUG) Log.d(TAG, "Querying children documents under " + documentId); if (BuildConfig.DEBUG) Log.d(TAG, "Querying children documents under " + documentId);
projection = (projection == null) ? DEFAULT_DOCUMENT_PROJECTION : projection; projection = (projection == null) ? DEFAULT_DOCUMENT_PROJECTION : projection;
@ -304,7 +304,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
mCache.throwLastExceptionIfAny(uri); mCache.throwLastExceptionIfAny(uri);
final LoadDocumentTask task = final LoadDocumentTask task =
new LoadDocumentTask(uri, mClient, mCache, mLoadDocumentCallback); new LoadDocumentTask(uri, mClient, mCache, mLoadDocumentCallback);
mTaskManager.runTask(uri, task); mTaskManager.runTask(uri, task);
cursor.setLoadingTask(task); cursor.setLoadingTask(task);
@ -321,7 +321,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
final Map<Uri, DocumentMetadata> childrenMap = metadata.getChildren(); final Map<Uri, DocumentMetadata> childrenMap = metadata.getChildren();
if (childrenMap == null || result.getState() == CacheResult.CACHE_EXPIRED) { if (childrenMap == null || result.getState() == CacheResult.CACHE_EXPIRED) {
final LoadChildrenTask task = final LoadChildrenTask task =
new LoadChildrenTask(metadata, mClient, mCache, mLoadChildrenCallback); new LoadChildrenTask(metadata, mClient, mCache, mLoadChildrenCallback);
mTaskManager.runTask(uri, task); mTaskManager.runTask(uri, task);
cursor.setLoadingTask(task); cursor.setLoadingTask(task);
@ -340,13 +340,13 @@ public class SambaDocumentsProvider extends DocumentsProvider {
} }
if (!isLoading && !docMap.isEmpty()) { if (!isLoading && !docMap.isEmpty()) {
LoadStatTask task = new LoadStatTask(docMap, mClient, LoadStatTask task = new LoadStatTask(docMap, mClient,
new OnTaskFinishedCallback<Map<Uri, DocumentMetadata>>() { new OnTaskFinishedCallback<Map<Uri, DocumentMetadata>>() {
@Override @Override
public void onTaskFinished( public void onTaskFinished(
@Status int status, Map<Uri, DocumentMetadata> item, Exception exception) { @Status int status, Map<Uri, DocumentMetadata> item, Exception exception) {
getContext().getContentResolver().notifyChange(notifyUri, null, false); getContext().getContentResolver().notifyChange(notifyUri, null, false);
} }
}); });
mTaskManager.runTask(uri, task); mTaskManager.runTask(uri, task);
cursor.setLoadingTask(task); cursor.setLoadingTask(task);
@ -381,7 +381,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
} }
private Object[] getDocumentValues( private Object[] getDocumentValues(
String[] projection, DocumentMetadata metadata) { String[] projection, DocumentMetadata metadata) {
Object[] row = new Object[projection.length]; Object[] row = new Object[projection.length];
for (int i = 0; i < projection.length; ++i) { for (int i = 0; i < projection.length; ++i) {
switch (projection[i]) { switch (projection[i]) {
@ -461,7 +461,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
final Uri uri = toUri(NetworkBrowser.SMB_BROWSING_URI.toString()); final Uri uri = toUri(NetworkBrowser.SMB_BROWSING_URI.toString());
if (mBrowsingStorage.isEmpty()) { if (mBrowsingStorage == null) {
AsyncTask serversTask = mNetworkBrowser.getServersAsync(mLoadSharesFinishedCallback); AsyncTask serversTask = mNetworkBrowser.getServersAsync(mLoadSharesFinishedCallback);
Bundle extra = new Bundle(); Bundle extra = new Bundle();
@ -475,7 +475,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
cursor.addRow(getCursorRowForServer(projection, server)); cursor.addRow(getCursorRowForServer(projection, server));
} }
mBrowsingStorage.clear(); mBrowsingStorage = null;
} }
return cursor; return cursor;
@ -483,15 +483,15 @@ public class SambaDocumentsProvider extends DocumentsProvider {
@Override @Override
public String createDocument(String parentDocumentId, String mimeType, String displayName) public String createDocument(String parentDocumentId, String mimeType, String displayName)
throws FileNotFoundException { throws FileNotFoundException {
try { try {
final Uri parentUri = toUri(parentDocumentId); final Uri parentUri = toUri(parentDocumentId);
boolean isDir = Document.MIME_TYPE_DIR.equals(mimeType); boolean isDir = Document.MIME_TYPE_DIR.equals(mimeType);
final DirectoryEntry entry = new DirectoryEntry( final DirectoryEntry entry = new DirectoryEntry(
isDir ? DirectoryEntry.DIR : DirectoryEntry.FILE, isDir ? DirectoryEntry.DIR : DirectoryEntry.FILE,
"", // comment "", // comment
displayName); displayName);
final Uri uri = DocumentMetadata.buildChildUri(parentUri, entry); final Uri uri = DocumentMetadata.buildChildUri(parentUri, entry);
if (isDir) { if (isDir) {
@ -617,15 +617,15 @@ public class SambaDocumentsProvider extends DocumentsProvider {
@Override @Override
public void removeDocument(String documentId, String parentDocumentId) public void removeDocument(String documentId, String parentDocumentId)
throws FileNotFoundException { throws FileNotFoundException {
// documentId is hierarchical. It can only have one parent. // documentId is hierarchical. It can only have one parent.
deleteDocument(documentId); deleteDocument(documentId);
} }
@Override @Override
public String moveDocument( public String moveDocument(
String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)
throws FileNotFoundException { throws FileNotFoundException {
try { try {
final Uri uri = toUri(sourceDocumentId); final Uri uri = toUri(sourceDocumentId);
final Uri targetParentUri = toUri(targetParentDocumentId); final Uri targetParentUri = toUri(targetParentDocumentId);
@ -637,19 +637,19 @@ public class SambaDocumentsProvider extends DocumentsProvider {
final List<String> pathSegmentsOfSource = uri.getPathSegments(); final List<String> pathSegmentsOfSource = uri.getPathSegments();
final List<String> pathSegmentsOfTargetParent = targetParentUri.getPathSegments(); final List<String> pathSegmentsOfTargetParent = targetParentUri.getPathSegments();
if (pathSegmentsOfSource.isEmpty() || if (pathSegmentsOfSource.isEmpty() ||
pathSegmentsOfTargetParent.isEmpty() || pathSegmentsOfTargetParent.isEmpty() ||
!Objects.equals(pathSegmentsOfSource.get(0), pathSegmentsOfTargetParent.get(0))) { !Objects.equals(pathSegmentsOfSource.get(0), pathSegmentsOfTargetParent.get(0))) {
throw new UnsupportedOperationException("Instance move across shares are not supported."); throw new UnsupportedOperationException("Instance move across shares are not supported.");
} }
final Uri targetUri = DocumentMetadata final Uri targetUri = DocumentMetadata
.buildChildUri(targetParentUri, uri.getLastPathSegment()); .buildChildUri(targetParentUri, uri.getLastPathSegment());
mClient.rename(uri.toString(), targetUri.toString()); mClient.rename(uri.toString(), targetUri.toString());
revokeDocumentPermission(sourceDocumentId); revokeDocumentPermission(sourceDocumentId);
getContext().getContentResolver() getContext().getContentResolver()
.notifyChange(toNotifyUri(DocumentMetadata.buildParentUri(uri)), null, false); .notifyChange(toNotifyUri(DocumentMetadata.buildParentUri(uri)), null, false);
getContext().getContentResolver().notifyChange(toNotifyUri(targetParentUri), null, false); getContext().getContentResolver().notifyChange(toNotifyUri(targetParentUri), null, false);
try (CacheResult result = mCache.get(uri)) { try (CacheResult result = mCache.get(uri)) {
@ -672,7 +672,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
@Override @Override
public ParcelFileDescriptor openDocument(String documentId, String mode, public ParcelFileDescriptor openDocument(String documentId, String mode,
CancellationSignal cancellationSignal) throws FileNotFoundException { CancellationSignal cancellationSignal) throws FileNotFoundException {
if (BuildConfig.DEBUG) Log.d(TAG, "Opening document " + documentId + " with mode " + mode); if (BuildConfig.DEBUG) Log.d(TAG, "Opening document " + documentId + " with mode " + mode);
try { try {
@ -680,7 +680,7 @@ public class SambaDocumentsProvider extends DocumentsProvider {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
OnTaskFinishedCallback<String> callback = OnTaskFinishedCallback<String> callback =
mode.contains("w") ? mWriteFinishedCallback : null; mode.contains("w") ? mWriteFinishedCallback : null;
return mClient.openProxyFile( return mClient.openProxyFile(
uri, uri,
mode, mode,
@ -709,13 +709,13 @@ public class SambaDocumentsProvider extends DocumentsProvider {
switch (mode) { switch (mode) {
case "r": { case "r": {
final ReadFileTask task = new ReadFileTask( final ReadFileTask task = new ReadFileTask(
uri, mClient, pipe[1], mBufferPool); uri, mClient, pipe[1], mBufferPool);
mTaskManager.runIoTask(task); mTaskManager.runIoTask(task);
} }
return pipe[0]; return pipe[0];
case "w": { case "w": {
final WriteFileTask task = final WriteFileTask task =
new WriteFileTask(uri, mClient, pipe[0], mBufferPool, mWriteFinishedCallback); new WriteFileTask(uri, mClient, pipe[0], mBufferPool, mWriteFinishedCallback);
mTaskManager.runIoTask(task); mTaskManager.runIoTask(task);
return pipe[1]; return pipe[1];
} }