diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/SambaProviderApplication.java b/app/src/main/java/com/google/android/sambadocumentsprovider/SambaProviderApplication.java index 74294fe..3ad1a44 100644 --- a/app/src/main/java/com/google/android/sambadocumentsprovider/SambaProviderApplication.java +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/SambaProviderApplication.java @@ -27,6 +27,7 @@ import android.net.NetworkRequest; import android.util.Log; import com.google.android.sambadocumentsprovider.SambaConfiguration.OnConfigurationChangedListener; +import com.google.android.sambadocumentsprovider.browsing.NetworkBrowser; import com.google.android.sambadocumentsprovider.cache.DocumentCache; import com.google.android.sambadocumentsprovider.nativefacade.CredentialCache; import com.google.android.sambadocumentsprovider.nativefacade.SambaMessageLooper; @@ -42,6 +43,7 @@ public class SambaProviderApplication extends Application { private SmbFacade mSambaClient; private ShareManager mShareManager; + private NetworkBrowser mNetworkBrowser; @Override public void onCreate() { @@ -64,6 +66,8 @@ public class SambaProviderApplication extends Application { mShareManager = new ShareManager(context, credentialCache); + mNetworkBrowser = new NetworkBrowser(mSambaClient, mTaskManager); + registerNetworkCallback(context); } @@ -129,6 +133,10 @@ public class SambaProviderApplication extends Application { return getApplication(context).mTaskManager; } + public static NetworkBrowser getNetworkBrowser(Context context) { + return getApplication(context).mNetworkBrowser; + } + private static SambaProviderApplication getApplication(Context context) { return ((SambaProviderApplication) context.getApplicationContext()); } diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/TaskManager.java b/app/src/main/java/com/google/android/sambadocumentsprovider/TaskManager.java index e53176e..4963627 100644 --- a/app/src/main/java/com/google/android/sambadocumentsprovider/TaskManager.java +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/TaskManager.java @@ -21,8 +21,6 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.AsyncTask.Status; import android.util.Log; -import com.google.android.sambadocumentsprovider.provider.ReadFileTask; -import com.google.android.sambadocumentsprovider.provider.WriteFileTask; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executor; diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/base/DirectoryEntry.java b/app/src/main/java/com/google/android/sambadocumentsprovider/base/DirectoryEntry.java index 5c0b03a..f30ea6d 100644 --- a/app/src/main/java/com/google/android/sambadocumentsprovider/base/DirectoryEntry.java +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/base/DirectoryEntry.java @@ -29,7 +29,7 @@ public class DirectoryEntry { @IntDef({WORKGROUP, SERVER, FILE_SHARE, PRINTER_SHARE, COMMS_SHARE, IPC_SHARE, DIR, FILE, LINK}) @Retention(RetentionPolicy.SOURCE) - @interface Type {} + public @interface Type {} public static final int WORKGROUP = 1; public static final int SERVER = 2; public static final int FILE_SHARE = 3; diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/BrowsingException.java b/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/BrowsingException.java new file mode 100644 index 0000000..8fb304b --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/BrowsingException.java @@ -0,0 +1,24 @@ +/* + * 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 . + */ + +package com.google.android.sambadocumentsprovider.browsing; + +class BrowsingException extends Exception { + BrowsingException(String message) { + super("Browsing failed: " + message); + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/MasterBrowsingProvider.java b/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/MasterBrowsingProvider.java new file mode 100644 index 0000000..d6cc630 --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/MasterBrowsingProvider.java @@ -0,0 +1,74 @@ +/* + * 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 . + */ + +package com.google.android.sambadocumentsprovider.browsing; + +import com.google.android.sambadocumentsprovider.base.DirectoryEntry; +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.List; + +class MasterBrowsingProvider implements NetworkBrowsingProvider { + private static final String MASTER_BROWSING_DIR = "smb://"; + + private final SmbClient mClient; + + MasterBrowsingProvider(SmbClient client) { + mClient = client; + } + + @Override + public List getServers() throws BrowsingException { + List serversList = new ArrayList<>(); + + try { + SmbDir rootDir = mClient.openDir(MASTER_BROWSING_DIR); + + List workgroups = getDirectoryChildren(rootDir); + for (DirectoryEntry workgroup : workgroups) { + if (workgroup.getType() == DirectoryEntry.WORKGROUP) { + List servers = getDirectoryChildren + (mClient.openDir(MASTER_BROWSING_DIR + workgroup.getName())); + + for (DirectoryEntry server : servers) { + if (server.getType() == DirectoryEntry.SERVER) { + serversList.add(server.getName()); + } + } + } + } + } catch (IOException e) { + throw new BrowsingException(e.getMessage()); + } + + return serversList; + } + + private static List getDirectoryChildren(SmbDir dir) throws IOException { + List children = new ArrayList<>(); + + DirectoryEntry currentEntry; + while ((currentEntry = dir.readDir()) != null) { + children.add(currentEntry); + } + + return children; + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/NetworkBrowser.java b/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/NetworkBrowser.java new file mode 100644 index 0000000..0083eeb --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/NetworkBrowser.java @@ -0,0 +1,96 @@ +/* + * 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 . + */ + +package com.google.android.sambadocumentsprovider.browsing; + +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; + +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.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.Map; +import java.util.concurrent.Future; + +public class NetworkBrowser { + public static final Uri SMB_BROWSING_URI = Uri.parse("smb://"); + + private static final String TAG = "NetworkBrowser"; + + private final NetworkBrowsingProvider mMasterProvider; + private final TaskManager mTaskManager; + + private final Map mTasks = new HashMap<>(); + + public NetworkBrowser(SmbClient client, TaskManager taskManager) { + mMasterProvider = new MasterBrowsingProvider(client); + mTaskManager = taskManager; + } + + public AsyncTask getServersAsync(OnTaskFinishedCallback> callback) { + AsyncTask> loadServersTask = new LoadServersTask(callback); + + mTaskManager.runTask(SMB_BROWSING_URI, loadServersTask); + + return loadServersTask; + } + + private List getServers() throws BrowsingException { + return mMasterProvider.getServers(); + } + + private class LoadServersTask extends AsyncTask> { + final OnTaskFinishedCallback> mCallback; + + private BrowsingException mException; + + LoadServersTask(OnTaskFinishedCallback> callback) { + mCallback = callback; + } + + List loadData() throws BrowsingException { + return getServers(); + } + + @Override + protected List doInBackground(Void... voids) { + try { + return loadData(); + } catch (BrowsingException e) { + Log.e(TAG, "Failed to load data for network browsing: ", e); + mException = e; + return null; + } + } + + protected void onPostExecute(List servers) { + if (servers != null) { + mCallback.onTaskFinished(OnTaskFinishedCallback.SUCCEEDED, servers, null); + } else { + mCallback.onTaskFinished(OnTaskFinishedCallback.FAILED, null, mException); + } + } + } +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/NetworkBrowsingProvider.java b/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/NetworkBrowsingProvider.java new file mode 100644 index 0000000..9aee90a --- /dev/null +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/browsing/NetworkBrowsingProvider.java @@ -0,0 +1,27 @@ +/* + * 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 . + */ + +package com.google.android.sambadocumentsprovider.browsing; + +import java.util.List; + +interface NetworkBrowsingProvider { + /** + * Returns unresolved host names. + */ + List getServers() throws BrowsingException; +} diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/document/DocumentMetadata.java b/app/src/main/java/com/google/android/sambadocumentsprovider/document/DocumentMetadata.java index 81c6f39..5ee55f3 100644 --- a/app/src/main/java/com/google/android/sambadocumentsprovider/document/DocumentMetadata.java +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/document/DocumentMetadata.java @@ -25,6 +25,8 @@ import android.system.StructStat; import android.text.TextUtils; import android.util.Log; import android.webkit.MimeTypeMap; + +import com.google.android.sambadocumentsprovider.R; import com.google.android.sambadocumentsprovider.base.DirectoryEntry; import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; import com.google.android.sambadocumentsprovider.nativefacade.SmbDir; @@ -32,7 +34,6 @@ import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -74,6 +75,18 @@ public class DocumentMetadata { return mEntry.getType() == DirectoryEntry.FILE_SHARE; } + public Integer getIconResourceId() { + switch (mEntry.getType()) { + case DirectoryEntry.SERVER: + return R.drawable.ic_server; + case DirectoryEntry.FILE_SHARE: + return R.drawable.ic_folder_shared; + default: + // Tells SAF to use the default icon. + return null; + } + } + public Long getLastModified() { final StructStat stat = mStat.get(); return (stat == null) ? null : TimeUnit.MILLISECONDS.convert(stat.st_mtime, TimeUnit.SECONDS); @@ -293,6 +306,10 @@ public class DocumentMetadata { return builder.build(); } + public static boolean isServerUri(Uri uri) { + return uri.getPathSegments().isEmpty() && !uri.getAuthority().isEmpty(); + } + public static DocumentMetadata fromUri(Uri uri, SmbClient client) throws IOException { final List pathSegments = uri.getPathSegments(); if (pathSegments.isEmpty()) { @@ -315,9 +332,17 @@ public class DocumentMetadata { return createShare(uri); } + public static DocumentMetadata createServer(Uri uri) { + return create(uri, DirectoryEntry.SERVER); + } + public static DocumentMetadata createShare(Uri uri) { + return create(uri, DirectoryEntry.FILE_SHARE); + } + + private static DocumentMetadata create(Uri uri, @DirectoryEntry.Type int type) { final DirectoryEntry entry = - new DirectoryEntry(DirectoryEntry.FILE_SHARE, "", uri.getLastPathSegment()); + new DirectoryEntry(type, "", uri.getLastPathSegment()); return new DocumentMetadata(uri, entry); } } diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerActivity.java b/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerActivity.java index 1b692f6..bf0ae0e 100644 --- a/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerActivity.java +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/mount/MountServerActivity.java @@ -56,6 +56,7 @@ import com.google.android.sambadocumentsprovider.cache.DocumentCache; import com.google.android.sambadocumentsprovider.document.DocumentMetadata; import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; import com.google.android.sambadocumentsprovider.provider.SambaDocumentsProvider; + import java.util.List; public class MountServerActivity extends AppCompatActivity { diff --git a/app/src/main/java/com/google/android/sambadocumentsprovider/provider/SambaDocumentsProvider.java b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/SambaDocumentsProvider.java index 3713690..787b143 100644 --- a/app/src/main/java/com/google/android/sambadocumentsprovider/provider/SambaDocumentsProvider.java +++ b/app/src/main/java/com/google/android/sambadocumentsprovider/provider/SambaDocumentsProvider.java @@ -27,6 +27,7 @@ import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.CancellationSignal; @@ -48,6 +49,7 @@ import com.google.android.sambadocumentsprovider.TaskManager; import com.google.android.sambadocumentsprovider.base.AuthFailedException; import com.google.android.sambadocumentsprovider.base.DirectoryEntry; import com.google.android.sambadocumentsprovider.base.DocumentCursor; +import com.google.android.sambadocumentsprovider.browsing.NetworkBrowser; import com.google.android.sambadocumentsprovider.cache.CacheResult; import com.google.android.sambadocumentsprovider.cache.DocumentCache; import com.google.android.sambadocumentsprovider.document.DocumentMetadata; @@ -55,12 +57,11 @@ import com.google.android.sambadocumentsprovider.document.LoadChildrenTask; import com.google.android.sambadocumentsprovider.base.OnTaskFinishedCallback; import com.google.android.sambadocumentsprovider.document.LoadDocumentTask; import com.google.android.sambadocumentsprovider.document.LoadStatTask; -import com.google.android.sambadocumentsprovider.nativefacade.SmbClient; import com.google.android.sambadocumentsprovider.nativefacade.SmbFacade; -import com.google.android.sambadocumentsprovider.nativefacade.SmbFile; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -87,7 +88,8 @@ public class SambaDocumentsProvider extends DocumentsProvider { Document.COLUMN_FLAGS, Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE, - Document.COLUMN_LAST_MODIFIED + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_ICON }; private final OnTaskFinishedCallback mLoadDocumentCallback = @@ -127,6 +129,22 @@ public class SambaDocumentsProvider extends DocumentsProvider { } }; + private final OnTaskFinishedCallback> mLoadSharesFinishedCallback = + new OnTaskFinishedCallback>() { + @Override + public void onTaskFinished( + @OnTaskFinishedCallback.Status int status, + @Nullable List item, + @Nullable Exception exception) { + if (BuildConfig.DEBUG) Log.d(TAG, "Browsing callback"); + + mBrowsingStorage = item; + + getContext().getContentResolver().notifyChange( + toNotifyUri(toUri(NetworkBrowser.SMB_BROWSING_URI.toString())), null, false); + } + }; + private final MountedShareChangeListener mShareChangeListener = new MountedShareChangeListener() { @Override public void onMountedServerChange() { @@ -142,6 +160,9 @@ public class SambaDocumentsProvider extends DocumentsProvider { private DocumentCache mCache; private TaskManager mTaskManager; private StorageManager mStorageManager; + private NetworkBrowser mNetworkBrowser; + + private List mBrowsingStorage = new ArrayList<>(); @Override public boolean onCreate() { @@ -155,6 +176,7 @@ public class SambaDocumentsProvider extends DocumentsProvider { mShareManager = SambaProviderApplication.getServerManager(context); mShareManager.addListener(mShareChangeListener); mStorageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + mNetworkBrowser = SambaProviderApplication.getNetworkBrowser(context); return mClient != null; } @@ -165,6 +187,15 @@ public class SambaDocumentsProvider extends DocumentsProvider { projection = (projection == null) ? DEFAULT_ROOT_PROJECTION : projection; MatrixCursor cursor = new MatrixCursor(projection, mShareManager.size()); + + cursor.addRow(new Object[] { + NetworkBrowser.SMB_BROWSING_URI.toString(), + NetworkBrowser.SMB_BROWSING_URI.toString(), + getContext().getResources().getString(R.string.browsing_root_name), + 0, + R.drawable.ic_cloud, + }); + for (String uri : mShareManager) { final String name; final Uri parsedUri = Uri.parse(uri); @@ -204,7 +235,15 @@ public class SambaDocumentsProvider extends DocumentsProvider { if (BuildConfig.DEBUG) Log.d(TAG, "Querying document: " + documentId); projection = (projection == null) ? DEFAULT_DOCUMENT_PROJECTION : projection; + final MatrixCursor cursor = new MatrixCursor(projection); final Uri uri = toUri(documentId); + + if (documentId.equals(NetworkBrowser.SMB_BROWSING_URI.toString())) { + cursor.addRow(getCursorRowForBrowsingRoot(projection)); + + return cursor; + } + try { try (CacheResult result = mCache.get(uri)) { @@ -221,7 +260,6 @@ public class SambaDocumentsProvider extends DocumentsProvider { metadata = result.getItem(); } - final MatrixCursor cursor = new MatrixCursor(projection); cursor.addRow(getDocumentValues(projection, metadata)); return cursor; @@ -239,13 +277,27 @@ public class SambaDocumentsProvider extends DocumentsProvider { if (BuildConfig.DEBUG) Log.d(TAG, "Querying children documents under " + documentId); projection = (projection == null) ? DEFAULT_DOCUMENT_PROJECTION : projection; + if (documentId.equals(NetworkBrowser.SMB_BROWSING_URI.toString())) { + return getFilesSharesCursor(projection); + } + final Uri uri = toUri(documentId); + try { + if (DocumentMetadata.isServerUri(uri)) { + try (final CacheResult result = mCache.get(uri)) { + if (result.getState() == CacheResult.CACHE_MISS) { + DocumentMetadata metadata = DocumentMetadata.createServer(uri); + mCache.put(metadata); + } + } + } + try (final CacheResult result = mCache.get(uri)) { boolean isLoading = false; - final DocumentCursor cursor = new DocumentCursor(projection); final Bundle extra = new Bundle(); final Uri notifyUri = toNotifyUri(uri); + final DocumentCursor cursor = new DocumentCursor(projection); if (result.getState() == CacheResult.CACHE_MISS) { // Last loading failed... Just feed the bitter fruit. @@ -359,11 +411,76 @@ public class SambaDocumentsProvider extends DocumentsProvider { case Document.COLUMN_LAST_MODIFIED: row[i] = metadata.getLastModified(); break; + case Document.COLUMN_ICON: + row[i] = metadata.getIconResourceId(); + break; } } return row; } + private Object[] getCursorRowForServer( + String[] projection, + String server) { + Object[] row = new Object[projection.length]; + + for (int i = 0; i < projection.length; ++i) { + switch (projection[i]) { + case Document.COLUMN_DOCUMENT_ID: + row[i] = NetworkBrowser.SMB_BROWSING_URI.toString() + server; + break; + case Document.COLUMN_DISPLAY_NAME: + row[i] = server.isEmpty() + ? getContext().getResources().getString(R.string.browsing_root_name) : server; + break; + case Document.COLUMN_FLAGS: + row[i] = 0; + break; + case Document.COLUMN_MIME_TYPE: + row[i] = Document.MIME_TYPE_DIR; + break; + case Document.COLUMN_SIZE: + case Document.COLUMN_LAST_MODIFIED: + row[i] = null; + break; + case Document.COLUMN_ICON: + row[i] = R.drawable.ic_server; + break; + } + } + + return row; + } + + private Object[] getCursorRowForBrowsingRoot(String[] projection) { + return getCursorRowForServer(projection, ""); + } + + private Cursor getFilesSharesCursor(String[] projection) { + final DocumentCursor cursor = new DocumentCursor(projection); + + final Uri uri = toUri(NetworkBrowser.SMB_BROWSING_URI.toString()); + + if (mBrowsingStorage.isEmpty()) { + AsyncTask serversTask = mNetworkBrowser.getServersAsync(mLoadSharesFinishedCallback); + + Bundle extra = new Bundle(); + extra.putBoolean(DocumentsContract.EXTRA_LOADING, true); + + cursor.setNotificationUri(getContext().getContentResolver(), toNotifyUri(uri)); + cursor.setExtras(extra); + cursor.setLoadingTask(serversTask); + } else { + for (String server : mBrowsingStorage) { + cursor.addRow(getCursorRowForServer(projection, server)); + } + + mBrowsingStorage.clear(); + } + + return cursor; + } + @Override public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { diff --git a/app/src/main/res/drawable/ic_cloud.xml b/app/src/main/res/drawable/ic_cloud.xml new file mode 100644 index 0000000..e7a8fed --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud.xml @@ -0,0 +1,28 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_server.xml b/app/src/main/res/drawable/ic_server.xml new file mode 100644 index 0000000..19c8638 --- /dev/null +++ b/app/src/main/res/drawable/ic_server.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ff2367..df3fedd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,4 +47,6 @@ Send feedback It needs a web browser to send feedback. + + Samba Shares