ExternalMonitor.kt 7.39 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
 * *************************************************************************
 *  NetworkMonitor.java
 * **************************************************************************
 *  Copyright © 2017 VLC authors and VideoLAN
 *  Author: Geoffrey Métais
 *
 *  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 2 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, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *  ***************************************************************************
 */

package org.videolan.vlc

import android.annotation.SuppressLint
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import android.net.Uri
import android.text.TextUtils
36
import androidx.core.content.ContextCompat
Geoffrey Métais's avatar
Geoffrey Métais committed
37
38
39
40
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
41
import kotlinx.coroutines.*
Geoffrey Métais's avatar
Geoffrey Métais committed
42
import kotlinx.coroutines.channels.BroadcastChannel
43
import kotlinx.coroutines.channels.Channel
Geoffrey Métais's avatar
Geoffrey Métais committed
44
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
45
import kotlinx.coroutines.channels.actor
46
import kotlinx.coroutines.flow.Flow
47
import kotlinx.coroutines.flow.asFlow
48
import org.videolan.medialibrary.interfaces.Medialibrary
49
import org.videolan.resources.ACTION_CHECK_STORAGES
50
import org.videolan.resources.AppContextProvider
Geoffrey Métais's avatar
Geoffrey Métais committed
51
import org.videolan.resources.util.getFromMl
52
import org.videolan.tools.*
53
import org.videolan.tools.livedata.LiveDataset
54
55
56
57
58
59
import org.videolan.vlc.gui.helpers.UiTools
import org.videolan.vlc.gui.helpers.hf.OtgAccess
import java.lang.ref.WeakReference

private const val TAG = "VLC/ExternalMonitor"

Geoffrey Métais's avatar
Geoffrey Métais committed
60
61
@ExperimentalCoroutinesApi
@ObsoleteCoroutinesApi
62
@SuppressLint("StaticFieldLeak")
63
object ExternalMonitor : BroadcastReceiver(), LifecycleObserver, CoroutineScope by MainScope() {
64
65

    private lateinit var ctx: Context
66
    private var registered = false
67
68
69
70
71

    private val actor = actor<DeviceAction>(capacity = Channel.CONFLATED) {
        for (action in channel) when (action){
            is MediaMounted -> {
                if (TextUtils.isEmpty(action.uuid)) return@actor
72
73
74
75
                val isNew = ctx.getFromMl {
                    val isNewForMl = !isDeviceKnown(action.uuid, action.path, true)
                    addDevice(action.uuid, action.path, true)
                    isNewForMl
76
                }
77
                if (isNew) notifyNewStorage(action)
78
79
80
            }
            is MediaUnmounted -> {
                delay(100L)
81
                Medialibrary.getInstance().removeDevice(action.uuid, action.path)
Geoffrey Métais's avatar
Geoffrey Métais committed
82
                storageChannel.safeOffer(action)
83
84
85
86
87
88
            }
        }
    }

    init {
        launch {
89
            ProcessLifecycleOwner.get().lifecycle.addObserver(this@ExternalMonitor)
90
91
92
93
94
        }
    }

    override fun onReceive(context: Context, intent: Intent) {
        if (!this::ctx.isInitialized) ctx = context.applicationContext
95
96
        when (intent.action) {
            Intent.ACTION_MEDIA_MOUNTED -> intent.data?.let { actor.offer(MediaMounted(it)) }
97
            Intent.ACTION_MEDIA_UNMOUNTED,
98
99
            Intent.ACTION_MEDIA_EJECT -> intent.data?.let {
                actor.offer(MediaUnmounted(it))
100
101
102
103
104
105
106
107
            }
            UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
                if (intent.hasExtra(UsbManager.EXTRA_DEVICE)) {
                    val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
                    devices.add(device)
                }
            }
            UsbManager.ACTION_USB_DEVICE_DETACHED -> if (intent.hasExtra(UsbManager.EXTRA_DEVICE)) {
108
                OtgAccess.otgRoot.value = null
109
110
111
112
113
114
                val device = intent.getParcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
                devices.remove(device)
            }
        }
    }

Geoffrey Métais's avatar
Geoffrey Métais committed
115
116
    private val storageChannel = BroadcastChannel<DeviceAction>(BUFFERED)
    val storageEvents : Flow<DeviceAction>
117
        get() = storageChannel.asFlow()
118
119
120
121
122
123
    private var storageObserver: WeakReference<Activity>? = null

    var devices = LiveDataset<UsbDevice>()

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
    fun register() {
124
        if (registered) return
125
        val ctx = AppContextProvider.appContext
126
127
128
129
130
131
132
133
        val storageFilter = IntentFilter(Intent.ACTION_MEDIA_MOUNTED)
        val otgFilter = IntentFilter(UsbManager.ACTION_USB_DEVICE_ATTACHED)
        storageFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED)
        storageFilter.addAction(Intent.ACTION_MEDIA_EJECT)
        otgFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
        storageFilter.addDataScheme("file")
        ctx.registerReceiver(this, storageFilter)
        ctx.registerReceiver(this, otgFilter)
134
        registered = true
135
136
137
        checkNewStorages(ctx)
    }

138
139
    @ExperimentalCoroutinesApi
    @ObsoleteCoroutinesApi
140
    private fun checkNewStorages(ctx: Context) {
141
        if (Medialibrary.getInstance().isStarted) {
Geoffrey Métais's avatar
Geoffrey Métais committed
142
            val scanOpt = if (Settings.showTvUi) ML_SCAN_ON
143
144
            else Settings.getInstance(ctx).getInt(KEY_MEDIALIBRARY_SCAN, -1)
            if (scanOpt == ML_SCAN_ON)
145
                AppScope.launch { ContextCompat.startForegroundService(ctx,Intent(ACTION_CHECK_STORAGES, null, ctx, MediaParsingService::class.java)) }
146
147
148
149
150
151
152
        }
        val usbManager = ctx.getSystemService(Context.USB_SERVICE) as? UsbManager ?: return
        devices.add(ArrayList(usbManager.deviceList.values))
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    internal fun unregister() {
153
        val ctx = AppContextProvider.appContext
154
155
156
        if (registered) try {
            ctx.unregisterReceiver(this)
        } catch (iae: IllegalArgumentException) {}
157
        registered = false
158
159
160
161
        devices.clear()
    }

    @Synchronized
162
    private fun notifyNewStorage(mediaMounted: MediaMounted) {
163
        val activity = storageObserver?.get() ?: return
164
        UiTools.newStorageDetected(activity, mediaMounted.path)
Geoffrey Métais's avatar
Geoffrey Métais committed
165
        storageChannel.safeOffer(mediaMounted)
166
167
168
169
170
171
172
173
174
    }

    @Synchronized
    fun subscribeStorageCb(observer: Activity) {
        storageObserver = WeakReference(observer)
    }

    @Synchronized
    fun unsubscribeStorageCb(observer: Activity) {
175
176
        if (storageObserver?.get() === observer) {
            storageObserver?.clear()
177
178
179
            storageObserver = null
        }
    }
180
}
181

182
fun containsDevice(devices: Array<String>, device: String): Boolean {
183
    if (devices.isNullOrEmpty()) return false
184
185
    for (dev in devices) if (device.startsWith(dev.removeFileProtocole())) return true
    return false
186
187
}

188
189
190
sealed class DeviceAction
class MediaMounted(val uri : Uri, val path : String = uri.path!!, val uuid : String = uri.lastPathSegment!!) : DeviceAction()
class MediaUnmounted(val uri : Uri, val path : String = uri.path!!, val uuid : String = uri.lastPathSegment!!) : DeviceAction()