Add Android readme and sample app

This commit is contained in:
Anay Wadhera
2024-01-10 15:25:19 -08:00
parent c2ade33c9c
commit b46ef89339
37 changed files with 1118 additions and 0 deletions
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NearbyShareDemo"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.NearbyShareDemo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,165 @@
package com.google.nearby.sharedemo
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.OpenableColumns
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.slice.Slice
import androidx.slice.widget.SliceView
import com.google.nearby.sharedemo.ui.theme.NearbyShareDemoTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: MainViewModel by viewModels(factoryProducer = { MainViewModel.Factory })
setContent {
NearbyShareDemoTheme {
// A surface container using the 'background' color from the theme
val state by viewModel.targetsFlow.collectAsState()
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
MainView(
state,
onShareTargetClicked = { data, intent ->
viewModel.onShareTargetClicked(data, this@MainActivity, intent)
})
}
}
}
}
}
@Composable
fun MainView(
state: SliceState,
onShareTargetClicked: (ShareTargetData, Intent) -> Unit,
) {
when (state) {
is SliceState.PermissionNeeded -> {
AndroidView(factory = {
val view = SliceView(it)
view.slice = state.slice
return@AndroidView view
})
}
is SliceState.Active -> {
var uris by rememberSaveable { mutableStateOf(listOf<Uri>()) }
val launcher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetMultipleContents()) {
uris = it
}
Column {
Button(onClick = { launcher.launch("*/*") }) { Text("Select files") }
if (uris.isNotEmpty()) {
for (uri in uris) {
Text(
uri.toString(),
style = MaterialTheme.typography.bodySmall
)
}
val sendIntent =
Intent("com.google.android.gms.SHARE_NEARBY").apply {
if (uris.size == 1) {
putExtra(Intent.EXTRA_STREAM, uris[0])
} else {
putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris.toArrayList())
}
type = "*/*"
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
val context = LocalContext.current
// We need to call sendIntent.migrateExtraStreamToClipData() to show the preview images on
// Android Q and above but the method is hidden. Hence we call PendingIntent.getActivity as
// a proxy/wrapper which calls the above method. This method can throw exception if files
// are too large.
// We need to call sendIntent.migrateExtraStreamToClipData() to show the preview images on
// Android Q and above but the method is hidden. Hence we call PendingIntent.getActivity as
// a proxy/wrapper which calls the above method. This method can throw exception if files
// are too large.
val pendingIntentFlags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
PendingIntent.getActivity(context.applicationContext, 0, sendIntent, pendingIntentFlags)
ShareDestinations(sendIntent, state.targets, onShareTargetClicked = { data, intent ->
for (uri in uris) {
context.grantUriPermission(
"com.google.android.gms",
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
onShareTargetClicked(data, intent)
})
}
}
}
}
}
@Composable
fun ShareDestinations(
sendIntent: Intent,
targets: Set<ShareTargetData>,
onShareTargetClicked: (ShareTargetData, Intent) -> Unit,
) {
Card(
modifier = Modifier.padding(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
if (targets.isEmpty()) {
Text("No devices nearby!")
return@Card
}
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 72.dp)) {
items(targets.toList()) {
ShareTarget(data = it, onShareTargetClicked = { data ->
onShareTargetClicked(data, sendIntent)
})
}
}
}
}
private fun <T> List<T>.toArrayList(): java.util.ArrayList<T> {
val list = java.util.ArrayList<T>()
list.addAll(this)
return list
}
sealed class SliceState {
data class PermissionNeeded(val slice: Slice) : SliceState()
data class Active(val targets: Set<ShareTargetData>) : SliceState()
}
@@ -0,0 +1,123 @@
package com.google.nearby.sharedemo
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.slice.Slice
import androidx.slice.SliceViewManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class MainViewModel(context: Context) : ViewModel() {
private val _targetsFlow: MutableStateFlow<SliceState> = MutableStateFlow(SliceState.Active(setOf()))
val targetsFlow: StateFlow<SliceState> = _targetsFlow.asStateFlow()
private var targetMapping = mapOf<ShareTargetData, PendingIntent>()
private val sliceManager = SliceViewManager.getInstance(context)
private val sliceCallback: (Slice?) -> Unit = {
targetMapping = parseSlice(it)
if (targetMapping.isEmpty() && it != null) {
_targetsFlow.value = SliceState.PermissionNeeded(it)
} else {
_targetsFlow.value = SliceState.Active(targetMapping.keys)
}
}
init {
sliceManager.registerSliceCallback(SCAN_SLICE_URI, sliceCallback)
val slice = sliceManager.bindSlice(SCAN_SLICE_URI)
targetMapping = parseSlice(slice)
if (targetMapping.isEmpty() && slice != null) {
_targetsFlow.value = SliceState.PermissionNeeded(slice)
} else {
_targetsFlow.value = SliceState.Active(targetMapping.keys)
}
}
/**
* This function is run just before the ViewModel is closed, allowing us to unpin the sharing
* slice.
*/
override fun onCleared() {
super.onCleared()
sliceManager.unregisterSliceCallback(SCAN_SLICE_URI, sliceCallback)
}
/**
* Called when a slice's share target is tapped, as represented by [ShareTarget].
*/
fun onShareTargetClicked(data: ShareTargetData, context: Context, sendIntent: Intent) {
targetMapping[data]!!.send(context, 0, sendIntent)
}
private fun parseSlice(slice: Slice?): Map<ShareTargetData, PendingIntent> {
android.util.Log.d("NSDemo", "slice: $slice")
if (slice == null) {
return mapOf()
}
val ret = mutableMapOf<ShareTargetData, PendingIntent>()
for (targetItem in slice.items.reversed()) {
if (!(targetItem.format == SLICE && targetItem.hints.containsAll(listOf(LIST_ITEM, ACTIVITY)))) {
continue
}
val targetSlice = targetItem.slice
var deviceName: String? = null
var action: PendingIntent? = null
var profileIcon: IconCompat? = null
for (item in targetSlice.items) {
if (item.format == TEXT && item.hints.contains(TITLE)) {
deviceName = item.text.toString()
}
if (item.format == ACTION && item.hints.containsAll(listOf(SHORTCUT, TITLE))) {
action = item.action
val iconSlice: Slice? = item.slice
if (iconSlice != null) {
for (iconitem in iconSlice.items) {
if (iconitem.format == IMAGE && iconitem.hints.contains(NO_TINT)) {
profileIcon = iconitem.icon
}
}
}
}
}
// Returns null if the data parsed from the slice is incomplete.
if (deviceName == null || action == null || profileIcon == null) {
continue
}
ret[ShareTargetData(profileIcon, deviceName)] = action
}
return ret
}
companion object {
private val SCAN_SLICE_URI: Uri =
Uri.parse("content://com.google.android.gms.nearby.sharing/scan")
// Slice parsing.
private const val SLICE = "slice"
private const val LIST_ITEM = "list_item"
private const val ACTIVITY = "activity"
private const val TEXT = "text"
private const val TITLE = "title"
private const val ACTION = "action"
private const val SHORTCUT = "shortcut"
private const val IMAGE = "image"
private const val NO_TINT = "no_tint"
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
MainViewModel(this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]!!)
}
}
}
}
@@ -0,0 +1,43 @@
package com.google.nearby.sharedemo
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.graphics.drawable.IconCompat
import androidx.core.graphics.drawable.toBitmap
@Composable
fun ShareTarget(data: ShareTargetData, onShareTargetClicked: (ShareTargetData) -> Unit) {
val context = LocalContext.current
Column(
modifier = Modifier
.padding(8.dp)
.clickable { onShareTargetClicked(data) },
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
data.profileIcon.loadDrawable(context)!!.toBitmap().asImageBitmap(),
contentDescription = null,
tint = Color.Unspecified,
)
Text(
data.deviceName,
style = MaterialTheme.typography.bodySmall,
)
}
}
data class ShareTargetData(
val profileIcon: IconCompat,
val deviceName: String,
)
@@ -0,0 +1,51 @@
package com.google.nearby.sharedemo.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme()
private val LightColorScheme = lightColorScheme()
@Composable
fun NearbyShareDemoTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
dynamicLightColorScheme(context)
}
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography(),
content = content
)
}
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Nearby Share Demo</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.NearbyShareDemo" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>