Merge pull request #2240 from anayw2001/main
Add Android Nearby Sharing sample app
@@ -0,0 +1,59 @@
|
||||
# Nearby Sharing Android API
|
||||
|
||||
## The slice API
|
||||
The nearby module in GMSCore provides a third-party accessible slice to bind to, to provide live data on nearby targets using Nearby Share. Clicking on any of these targets will take you to the main Nearby Share screen, where the transfer will begin and continually update the progress.
|
||||
|
||||
The slice URI is `content://com.google.android.gms.nearby.sharing/scan`. Upon first launch after install, you will be prompted to grant permission to the client application. After that, the slice will be kept up to date with the latest targets nearby.
|
||||
|
||||
## How to use the API ##
|
||||
In Kotlin:
|
||||
```kotlin
|
||||
// Derive the Slice URI.
|
||||
val sliceUri = Uri.parse("content://com.google.android.gms.nearby.sharing/scan")
|
||||
// Get the SliceViewManager
|
||||
val sliceManager = SliceViewManager.getInstance(context)
|
||||
// Pin the slice and register your callback.
|
||||
sliceManager.registerSliceCallback(sliceUri, { slice: Slice? ->
|
||||
if (slice == null) {
|
||||
return
|
||||
}
|
||||
for (targetItem in slice.items.reversed()) {
|
||||
// Each row containing a target has the hints LIST_ITEM and ACTIVITY.
|
||||
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) {
|
||||
// The slice item of the target's device name contains the TITLE hint.
|
||||
if (item.format == TEXT && item.hints.contains(TITLE)) {
|
||||
deviceName = item.text.toString()
|
||||
}
|
||||
// The slice item of the target action contains the SHORTCUT and TITLE hints.
|
||||
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) {
|
||||
// The target's icon is indicated by the IMAGE slice item format and the NO_TINT hint.
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
// Remember to bind the slice after you pin it to avoid race conditions!
|
||||
val slice = sliceManager.bindSlice(sliceUri)
|
||||
```
|
||||
Refer to the [SliceViewManager#registerSliceCallback() documentation](https://developer.android.com/reference/androidx/slice/SliceViewManager#registerSliceCallback(android.net.Uri,androidx.slice.SliceViewManager.SliceCallback)) for more information about the race condition described above.
|
||||
@@ -0,0 +1,16 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.idea
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
@@ -0,0 +1,25 @@
|
||||
# Nearby Share sample app
|
||||
|
||||
This app is a demonstration of how to bind to and use the Nearby Share slice.
|
||||
|
||||
## Build instructions
|
||||
From the root of the repository:
|
||||
```
|
||||
// Run this before importing into Android Studio!
|
||||
$ cd sharing/android/example
|
||||
$ gradle wrapper
|
||||
$ ./gradlew build
|
||||
```
|
||||
For the gradle wrapper, we recommend Gradle 8.0+.
|
||||
|
||||
## Key callouts
|
||||
MainViewModel - contains the business logic for processing and binding to the slice, as well as the lifecycle of the slice.
|
||||
|
||||
MainActivity - hosts the `MainView` composable and provides the intent to fill in the slice's PendingIntent action with, providing the data to send via Nearby Share.
|
||||
|
||||
## Screenshots
|
||||
<p align="left">
|
||||
<img alt="No devices available screenshot" src="screenshots/no_devices.png" width="33%">
|
||||
<img alt="Devices available screenshot" src="screenshots/devices.png" width="33%">
|
||||
<img alt="Consent prompt screenshot" src="screenshots/consent.png" width="33%">
|
||||
</p>
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,62 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.google.nearby.sharedemo"
|
||||
compileSdk = 33
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.google.nearby.sharedemo"
|
||||
minSdk = 30
|
||||
targetSdk = 33
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.3"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||
implementation("androidx.activity:activity-compose:1.7.2")
|
||||
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.slice:slice-core:1.1.0-alpha02")
|
||||
implementation("androidx.slice:slice-view:1.1.0-alpha02")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
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>
|
||||
@@ -0,0 +1,5 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id("com.android.application") version "8.1.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.8.10" apply false
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,17 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Nearby Share Demo"
|
||||
include(":app")
|
||||