Skip to content

Commit a2b6f19

Browse files
committed
SMB device discovery strategy using mDNS/NsdManager
Fixes #4488.
1 parent f12fd75 commit a2b6f19

File tree

6 files changed

+1205
-18
lines changed

6 files changed

+1205
-18
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@
2727

2828
<uses-permission android:name="android.permission.WAKE_LOCK" />
2929
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
30+
<uses-permission android:name="android.permission.INTERNET" />
31+
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
32+
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
33+
android:usesPermissionFlags="neverForLocation"
34+
tools:targetApi="33" />
3035
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
3136
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
3237
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
3338
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
34-
<uses-permission android:name="android.permission.INTERNET" />
3539
<uses-permission android:name="com.amaze.cloud.permission.ACCESS_PROVIDER" />
3640
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
3741
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

app/src/main/java/com/amaze/filemanager/ui/dialogs/SmbSearchDialog.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ package com.amaze.filemanager.ui.dialogs
2323
import android.app.Activity
2424
import android.app.Dialog
2525
import android.content.Context
26-
import android.graphics.Color
2726
import android.os.Bundle
2827
import android.view.LayoutInflater
2928
import android.view.View
3029
import android.view.ViewGroup
3130
import android.widget.Toast
3231
import androidx.appcompat.widget.AppCompatImageView
3332
import androidx.appcompat.widget.AppCompatTextView
33+
import androidx.core.graphics.toColorInt
3434
import androidx.fragment.app.DialogFragment
3535
import androidx.lifecycle.MutableLiveData
3636
import androidx.lifecycle.ViewModel
@@ -142,11 +142,8 @@ class SmbSearchDialog : DialogFragment() {
142142
context: Context,
143143
) : RecyclerView.Adapter<ViewHolder>() {
144144
private val items: MutableList<ComputerParcelable> = ArrayList()
145-
private val mInflater: LayoutInflater
146-
147-
init {
148-
mInflater = context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
149-
}
145+
private val inflater: LayoutInflater =
146+
context.getSystemService(Activity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
150147

151148
/**
152149
* Called by [ComputerParcelableViewModel], add found computer to list view
@@ -194,12 +191,12 @@ class SmbSearchDialog : DialogFragment() {
194191
val view: View
195192
return when (viewType) {
196193
VIEW_PROGRESSBAR -> {
197-
view = mInflater.inflate(R.layout.smb_progress_row, parent, false)
194+
view = inflater.inflate(R.layout.smb_progress_row, parent, false)
198195
ViewHolder(view)
199196
}
200197
else -> {
201198
view =
202-
mInflater.inflate(R.layout.smb_computers_row, parent, false)
199+
inflater.inflate(R.layout.smb_computers_row, parent, false)
203200
ElementViewHolder(view)
204201
}
205202
}
@@ -229,7 +226,7 @@ class SmbSearchDialog : DialogFragment() {
229226
holder.txtTitle.text = name
230227
holder.image.setImageResource(R.drawable.ic_settings_remote_white_48dp)
231228
if (utilsProvider.appTheme == AppTheme.LIGHT) {
232-
holder.image.setColorFilter(Color.parseColor("#666666"))
229+
holder.image.setColorFilter("#666666".toColorInt())
233230
}
234231
holder.txtDesc.text = addr
235232
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright (C) 2014-2024 Arpit Khurana <[email protected]>, Vishal Nehra <[email protected]>,
3+
* Emmanuel Messulam<[email protected]>, Raymond Lai <airwave209gt at gmail.com> and Contributors.
4+
*
5+
* This file is part of Amaze File Manager.
6+
*
7+
* Amaze File Manager is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.amaze.filemanager.utils.smb
22+
23+
import android.content.Context.NSD_SERVICE
24+
import android.content.Context.WIFI_SERVICE
25+
import android.net.nsd.NsdManager
26+
import android.net.nsd.NsdServiceInfo
27+
import android.net.wifi.WifiManager
28+
import android.os.Build.VERSION.SDK_INT
29+
import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
30+
import com.amaze.filemanager.application.AppConfig
31+
import com.amaze.filemanager.utils.ComputerParcelable
32+
import org.slf4j.Logger
33+
import org.slf4j.LoggerFactory
34+
35+
/**
36+
* [SmbDeviceScannerObservable.DiscoverDeviceStrategy] implementation using Android's
37+
* [NsdManager] to discover SMB devices using mDNS/Bonjour/ZeroConf.
38+
*
39+
* @see SmbDeviceScannerObservable
40+
* @see NsdManager
41+
*
42+
*/
43+
class NsdManagerDiscoverDeviceStrategy : SmbDeviceScannerObservable.DiscoverDeviceStrategy {
44+
companion object {
45+
internal const val SERVICE_TYPE_SMB = "_smb._tcp."
46+
private val logger: Logger =
47+
LoggerFactory.getLogger(NsdManagerDiscoverDeviceStrategy::class.java)
48+
}
49+
50+
private val wifiManager: WifiManager =
51+
AppConfig.getInstance().applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
52+
private val nsdManager: NsdManager =
53+
AppConfig.getInstance().applicationContext.getSystemService(NSD_SERVICE) as NsdManager
54+
55+
private var multicastLock: WifiManager.MulticastLock? = null
56+
private var discoveryListener: NsdManager.DiscoveryListener? = null
57+
58+
override fun discoverDevices(callback: (ComputerParcelable) -> Unit) {
59+
multicastLock =
60+
wifiManager.createMulticastLock("smb_mdns_discovery").apply {
61+
setReferenceCounted(true)
62+
}
63+
multicastLock?.acquire()
64+
discoveryListener = createDiscoveryListener(callback)
65+
nsdManager.discoverServices(
66+
SERVICE_TYPE_SMB,
67+
NsdManager.PROTOCOL_DNS_SD,
68+
discoveryListener,
69+
)
70+
}
71+
72+
override fun onCancel() {
73+
discoveryListener?.let {
74+
nsdManager.stopServiceDiscovery(it)
75+
discoveryListener = null
76+
}
77+
multicastLock?.let {
78+
if (it.isHeld) {
79+
it.release()
80+
}
81+
}
82+
}
83+
84+
/**
85+
* Creates a new [NsdManager.DiscoveryListener] to handle service discovery events.
86+
*
87+
* For backward compatibility, uses [NsdManager.ResolveListener] to resolve services
88+
* and perform the callback.
89+
*/
90+
private fun createDiscoveryListener(callback: (ComputerParcelable) -> Unit): NsdManager.DiscoveryListener {
91+
return object : NsdManager.DiscoveryListener {
92+
override fun onServiceFound(serviceInfo: NsdServiceInfo) {
93+
@Suppress("DEPRECATION")
94+
nsdManager.resolveService(
95+
serviceInfo,
96+
object : NsdManager.ResolveListener {
97+
override fun onServiceResolved(resolvedServiceInfo: NsdServiceInfo) {
98+
val host =
99+
if (SDK_INT >= UPSIDE_DOWN_CAKE) {
100+
resolvedServiceInfo.hostAddresses.firstOrNull()
101+
} else {
102+
resolvedServiceInfo.host
103+
}
104+
if (host != null && host.hostAddress?.isNotEmpty() == true) {
105+
val computer =
106+
ComputerParcelable(
107+
name = resolvedServiceInfo.serviceName,
108+
addr = host.hostAddress!!,
109+
)
110+
callback(computer)
111+
}
112+
}
113+
114+
override fun onResolveFailed(
115+
serviceInfo: NsdServiceInfo?,
116+
errorCode: Int,
117+
) {
118+
logger.error(
119+
"Service resolve failed: ${serviceInfo?.serviceName} with error code: $errorCode",
120+
)
121+
}
122+
},
123+
)
124+
}
125+
126+
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
127+
logger.debug("Service lost: ${serviceInfo?.serviceName}")
128+
}
129+
130+
override fun onStartDiscoveryFailed(
131+
serviceType: String,
132+
errorCode: Int,
133+
) {
134+
logger.error("Service discovery start failed: $serviceType with error code: $errorCode")
135+
nsdManager.stopServiceDiscovery(this)
136+
}
137+
138+
override fun onStopDiscoveryFailed(
139+
serviceType: String,
140+
errorCode: Int,
141+
) {
142+
logger.debug("Service discovery stop failed: $serviceType with error code: $errorCode")
143+
nsdManager.stopServiceDiscovery(this)
144+
}
145+
146+
override fun onDiscoveryStarted(serviceType: String?) = logger.debug("Service discovery started: $serviceType")
147+
148+
override fun onDiscoveryStopped(serviceType: String?) = logger.debug("Service discovery stopped: $serviceType")
149+
}
150+
}
151+
}

app/src/main/java/com/amaze/filemanager/utils/smb/SmbDeviceScannerObservable.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ package com.amaze.filemanager.utils.smb
2222

2323
import androidx.annotation.VisibleForTesting
2424
import com.amaze.filemanager.utils.ComputerParcelable
25-
import com.amaze.filemanager.utils.smb.SmbDeviceScannerObservable.DiscoverDeviceStrategy
2625
import io.reactivex.Observable
2726
import io.reactivex.Observer
2827
import io.reactivex.disposables.Disposable
@@ -55,6 +54,7 @@ class SmbDeviceScannerObservable : Observable<ComputerParcelable>() {
5554
arrayOf(
5655
WsddDiscoverDeviceStrategy(),
5756
SameSubnetDiscoverDeviceStrategy(),
57+
NsdManagerDiscoverDeviceStrategy(),
5858
)
5959
@VisibleForTesting set
6060

@@ -86,19 +86,22 @@ class SmbDeviceScannerObservable : Observable<ComputerParcelable>() {
8686
*/
8787
override fun subscribeActual(observer: Observer<in ComputerParcelable>) {
8888
this.observer = observer
89+
observer.onSubscribe(Disposables.empty())
8990
this.disposable =
9091
merge(
9192
discoverDeviceStrategies.map { strategy ->
92-
fromCallable {
93+
create<ComputerParcelable> { emitter ->
9394
strategy.discoverDevices { addr ->
94-
observer.onNext(ComputerParcelable(addr.addr, addr.name))
95+
if (!emitter.isDisposed) {
96+
emitter.onNext(addr)
97+
}
9598
}
99+
emitter.setCancellable { strategy.onCancel() }
96100
}.subscribeOn(Schedulers.io())
97101
},
98-
).observeOn(Schedulers.computation()).doOnComplete {
99-
discoverDeviceStrategies.forEach { strategy ->
100-
strategy.onCancel()
101-
}
102-
}.subscribe()
102+
).observeOn(Schedulers.computation()).subscribe(
103+
{ computer -> observer.onNext(computer) },
104+
{ error -> observer.onError(error) },
105+
)
103106
}
104107
}

0 commit comments

Comments
 (0)