first commit

This commit is contained in:
ksenia312
2024-01-31 14:16:22 +01:00
commit 541fbeaaf3
132 changed files with 6463 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.
+21
View File
@@ -0,0 +1,21 @@
Copyright 2024 Nikitina Kseniia (xenikii.one).
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+7
View File
@@ -0,0 +1,7 @@
# Nearby Service
<h3>Connecting phones in a P2P network</h3>
[![Xenikii Website](https://img.shields.io/badge/-xenikii.one-bd2727?style=flat&logoColor=white)](https://xenikii.one)
[![LICENSE BSD](https://img.shields.io/badge/License-BSD-4577d9)](https://github.com/ksenia312/nearby_service/blob/main/LICENSE)
+4
View File
@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+9
View File
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
+70
View File
@@ -0,0 +1,70 @@
group 'com.xenikii.nearby_service'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdk 33
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
test.java.srcDirs += 'src/test/kotlin'
}
defaultConfig {
minSdkVersion 24
}
dependencies {
testImplementation 'org.jetbrains.kotlin:kotlin-test'
testImplementation 'org.mockito:mockito-core:5.0.0'
}
testOptions {
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen { false }
showStandardStreams = true
}
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.6.1'
}
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+240
View File
@@ -0,0 +1,240 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+91
View File
@@ -0,0 +1,91 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+1
View File
@@ -0,0 +1 @@
rootProject.name = 'nearby_service'
+16
View File
@@ -0,0 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xenikii.nearby_service">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-feature
android:name="android.hardware.wifi.direct"
android:required="true" />
</manifest>
@@ -0,0 +1,29 @@
package com.xenikii.nearby_service
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pInfo
import org.json.JSONObject
fun WifiP2pDevice.toJsonString(): String {
val jsonObject = JSONObject()
jsonObject.put("deviceName", deviceName)
jsonObject.put("deviceAddress", deviceAddress)
jsonObject.put("isGroupOwner", isGroupOwner)
jsonObject.put("isServiceDiscoveryCapable", isServiceDiscoveryCapable)
jsonObject.put("primaryDeviceType", primaryDeviceType)
jsonObject.put("secondaryDeviceType", secondaryDeviceType)
jsonObject.put("wpsDisplaySupported", wpsDisplaySupported())
jsonObject.put("wpsPbcSupported", wpsPbcSupported())
jsonObject.put("wpsKeypadSupported", wpsKeypadSupported())
jsonObject.put("status", status)
return jsonObject.toString()
}
fun WifiP2pInfo.toJsonString(): String {
val jsonObject = JSONObject()
jsonObject.put("groupFormed", groupFormed)
jsonObject.put("groupOwnerAddress", groupOwnerAddress)
jsonObject.put("isGroupOwner", isGroupOwner)
return jsonObject.toString()
}
@@ -0,0 +1,38 @@
package com.xenikii.nearby_service
import android.util.Log
const val TAG = "NearbyService"
/**
* Class for systemizing log levels.
*/
enum class LogLevel(val value: Int) {
DEBUG(1),
INFO(2),
ERROR(3),
DISABLED(4),
}
class Logger {
companion object {
var level = LogLevel.DEBUG
fun d(message: String) {
if (level.value <= LogLevel.DEBUG.value) {
Log.d(TAG, message)
}
}
fun i(message: String) {
if (level.value <= LogLevel.INFO.value) {
Log.i(TAG, message)
}
}
fun e(message: String) {
if (level.value <= LogLevel.ERROR.value) {
Log.e(TAG, message)
}
}
}
}
@@ -0,0 +1,112 @@
package com.xenikii.nearby_service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList
import android.net.wifi.p2p.WifiP2pInfo
import android.net.wifi.p2p.WifiP2pManager
import android.net.wifi.p2p.WifiP2pManager.Channel
import android.os.Build
/**
* Receiver of [WifiP2pManager] changes.
*/
class NearbyServiceBroadcastReceiver(
private val wifiManager: WifiP2pManager,
private val wifiChannel: Channel,
private val permissionsHandler: NearbyServicePermissionsHandler,
) : BroadcastReceiver() {
var peers: MutableList<String> = mutableListOf()
var connectedDevice: WifiP2pDevice? = null
var currentDevice: WifiP2pDevice? = null
var wifiInfo: WifiP2pInfo? = null
override fun onReceive(context: Context, intent: Intent) {
Logger.i("Received action ${intent.action?.replace("android.net.wifi.p2p.", "")}")
when (intent.action) {
WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION -> {
logState(intent)
writeConnectionInfo()
}
WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION -> {
writeDevices()
}
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION -> {
writeConnectionInfo()
}
WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION -> {
writeConnectionInfo()
}
WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION -> {
writeCurrentDevice(intent)
}
}
}
private fun logState(intent: Intent) {
when (intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1)) {
WifiP2pManager.WIFI_P2P_STATE_ENABLED -> {
Logger.d("P2P state enabled")
}
else -> {
Logger.d("P2P state disabled")
}
}
}
private fun writeCurrentDevice(intent: Intent) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION")
currentDevice = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_DEVICE)
}
}
private fun writeDevices() {
try {
wifiManager.requestPeers(
wifiChannel
) { newPeers: WifiP2pDeviceList ->
val list: MutableList<String> = mutableListOf()
if (newPeers.deviceList.isEmpty() && connectedDevice != null) {
connectedDevice = null
}
for (device: WifiP2pDevice in newPeers.deviceList) {
list.add(device.toJsonString())
if (device.status == WifiP2pDevice.CONNECTED) {
connectedDevice = device
} else if (device.deviceAddress == connectedDevice?.deviceAddress &&
device.status != WifiP2pDevice.CONNECTED
) {
connectedDevice = null
}
}
peers = list
}
} catch (e: SecurityException) {
if (!permissionsHandler.checkPermissions()) {
Logger.e("No permission to call 'writeDevices'")
permissionsHandler.requestPermissions()
}
}
}
private fun writeConnectionInfo() {
wifiManager.requestConnectionInfo(wifiChannel) { info: WifiP2pInfo ->
if (!info.groupFormed && wifiInfo?.groupFormed == true) {
writeDevices()
}
wifiInfo = info
}
}
}
@@ -0,0 +1,292 @@
package com.xenikii.nearby_service
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.wifi.WifiManager
import android.net.wifi.WpsInfo
import android.net.wifi.p2p.WifiP2pConfig
import android.net.wifi.p2p.WifiP2pManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.future.await
/**
* General Manager of Wi-fi Direct local network operations.
*/
class NearbyServiceManager(private var context: Context) {
private lateinit var wifiManager: WifiP2pManager
private lateinit var wifiChannel: WifiP2pManager.Channel
private lateinit var receiver: NearbyServiceBroadcastReceiver
private val intentFilter = IntentFilter()
private var permissionsHandler = NearbyServicePermissionsHandler(context)
private var activityPluginBinding: ActivityPluginBinding? = null
/**
* Sets [binding] to [activityPluginBinding] and [permissionsHandler].
* Adds permissions result listener to [binding].
*/
fun setBinding(binding: ActivityPluginBinding) {
activityPluginBinding = binding
activityPluginBinding?.addRequestPermissionsResultListener(permissionsHandler)
permissionsHandler.activity = binding.activity
}
/**
* Removes permissions result listener to [activityPluginBinding].
* Sets [activityPluginBinding] to null.
*/
fun removeBinding() {
activityPluginBinding?.removeRequestPermissionsResultListener(permissionsHandler)
activityPluginBinding = null
}
/**
* Initializes everything for [WifiManager] to work.
*/
fun initialize(result: Result, logLevel: String) {
Logger.level = LogLevel.valueOf(logLevel.uppercase())
addWifiActions()
initWifiManager()
initReceiver()
result.success(true)
}
/**
* Requesting permissions with [permissionsHandler].
*/
suspend fun requestPermissions(): Boolean {
return permissionsHandler.requestPermissionsAsync().await()
}
/**
* Checking if Wi-fi is enabled now.
*/
fun checkWifiService(result: Result) {
result.success(
(context.getSystemService(Context.WIFI_SERVICE) as WifiManager).isWifiEnabled
)
}
/**
* Returns info about a current device in format WifiP2pDevice.toJsonString().
*
* Note!
* The field **deviceAddress** will always be 02:00:00:00:00:00 for privacy issues.
*
* Note!
* If the SDK version is less than 29 (Q), tries to return a current device from [receiver].
* It also may be null.
*/
fun getCurrentDevice(result: Result) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
wifiManager.requestDeviceInfo(wifiChannel) { device ->
result.success(device?.toJsonString())
}
} else {
result.success(receiver.currentDevice?.toJsonString())
}
} catch (e: SecurityException) {
if (!permissionsHandler.checkPermissions()) {
Logger.e("No permission to call 'discover'")
permissionsHandler.requestPermissions()
}
}
}
/**
* Opens the phone settings under Wi-fi.
*/
fun openWifiSettings(result: Result) {
activityPluginBinding?.activity?.startActivity(Intent(Settings.ACTION_WIFI_SETTINGS))
result.success(true)
}
/**
* Start discovery for peers in Wi-fi Direct scope.
*
* Note!
* All permissions from [NearbyServicePermissionsHandler] are required.
*/
fun discover(result: Result) {
try {
wifiManager.discoverPeers(
wifiChannel, getActionListener(result)
)
} catch (e: SecurityException) {
if (!permissionsHandler.checkPermissions()) {
Logger.e("No permission to call 'discover'")
permissionsHandler.requestPermissions()
}
}
}
/**
* Stop discovery for peers in Wi-fi Direct scope.
*/
fun stopDiscovery(result: Result) {
wifiManager.stopPeerDiscovery(
wifiChannel, getActionListener(result)
)
}
/**
* Returns peers from [NearbyServiceBroadcastReceiver].
*/
fun getPeers(result: Result) {
result.success(receiver.peers)
}
/**
* Returns connection info from [NearbyServiceBroadcastReceiver] in json string.
*/
fun getConnectionInfo(result: Result) {
val info = receiver.wifiInfo?.toJsonString()
result.success(info)
}
/**
* Connects to provided [deviceAddress] in Wi-fi Direct scope.
*/
fun connect(result: Result, deviceAddress: String) {
val config = WifiP2pConfig()
if (receiver.connectedDevice?.deviceAddress == deviceAddress) {
Logger.i("Already connected to the device $deviceAddress")
result.success(true)
return
}
val actionListener = getActionListener(
result,
"Connected to device $deviceAddress",
"Connecting to device $deviceAddress failed"
)
config.deviceAddress = deviceAddress
config.wps.setup = WpsInfo.PBC
try {
wifiChannel.also { wifiChannel: WifiP2pManager.Channel ->
wifiManager.connect(wifiChannel, config, actionListener)
}
} catch (e: SecurityException) {
if (!permissionsHandler.checkPermissions()) {
Logger.e("No permission to call 'connect'")
permissionsHandler.requestPermissions()
}
}
}
/**
* Disconnect from a previous device in Wi-fi Direct scope.
*/
fun disconnect(result: Result? = null) {
val actionListener = getActionListener(
result, "Disconnected from last device", "Failed to disconnect"
)
wifiManager.removeGroup(wifiChannel, actionListener)
}
private fun addWifiActions() {
intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION)
intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION)
intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)
intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION)
intentFilter.addAction(WifiP2pManager.WIFI_P2P_DISCOVERY_CHANGED_ACTION)
}
private fun initWifiManager() {
wifiManager = context.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
wifiChannel = wifiManager.initialize(context, Looper.getMainLooper(), null)
}
private fun initReceiver() {
receiver = NearbyServiceBroadcastReceiver(
wifiManager,
wifiChannel,
permissionsHandler,
)
context.registerReceiver(receiver, intentFilter)
}
private fun getActionListener(
result: Result?,
successMessage: String? = null,
errorMessage: String? = null,
): WifiP2pManager.ActionListener {
return object : WifiP2pManager.ActionListener {
override fun onSuccess() {
if (successMessage != null) {
Logger.i(successMessage)
}
result?.success(true)
}
override fun onFailure(reasonCode: Int) {
if (errorMessage != null) {
Logger.e("ERROR: $errorMessage Reason code: $reasonCode")
}
result?.success(false)
}
}
}
var peersHandler = object : EventChannel.StreamHandler {
private var handler: Handler = Handler(Looper.getMainLooper())
private var eventSink: EventChannel.EventSink? = null
val postCallback = object : Runnable {
override fun run() {
handler.post { eventSink?.success("${receiver.peers}") }
handler.postDelayed(this, 1000)
}
}
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
onCancel(null)
Logger.d("Start listening peers")
eventSink = sink
handler.postDelayed(postCallback, 1000)
}
override fun onCancel(p0: Any?) {
Logger.d("Kill last process listening peers")
eventSink = null
handler.removeCallbacks(postCallback)
}
}
var connectedDeviceInfoHandler = object : EventChannel.StreamHandler {
private var handler: Handler = Handler(Looper.getMainLooper())
private var eventSink: EventChannel.EventSink? = null
val postCallback = object : Runnable {
override fun run() {
handler.post { eventSink?.success(receiver.connectedDevice?.toJsonString()) }
handler.postDelayed(this, 1000)
}
}
override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
onCancel(null)
eventSink = sink
Logger.d("Listen connected device")
handler.postDelayed(postCallback, 1000)
}
override fun onCancel(p0: Any?) {
Logger.d("Kill last process connected device")
eventSink = null
handler.removeCallbacks(postCallback)
}
}
}
@@ -0,0 +1,151 @@
package com.xenikii.nearby_service
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.annotation.RequiresApi
import io.flutter.plugin.common.PluginRegistry
import kotlinx.coroutines.future.await
import java.util.concurrent.CompletableFuture
/**
* Class representing permission.
* [name] is official permission name from [Manifest].
* [code] is the value with which the permission will be requested - requestCode.
*/
class AppPermission(val name: String, val code: Int) {
val future = CompletableFuture<Boolean>()
}
/**
* The class responsible for plugin permissions.
* It contains different ways of checking and requesting them.
* Before you use it, make sure you set the [activity] externally.
*/
class NearbyServicePermissionsHandler(private var context: Context) :
PluginRegistry.RequestPermissionsResultListener {
var activity: Activity? = null
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private val nearbyPermission = AppPermission(Manifest.permission.NEARBY_WIFI_DEVICES, 98)
private val locationPermission = AppPermission(Manifest.permission.ACCESS_FINE_LOCATION, 99)
/**
* Sync checking permissions.
*/
fun checkPermissions(): Boolean {
val locationGranted = checkLocationPermission()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
checkNearbyPermission() && locationGranted
} else {
return locationGranted
}
}
/**
* Sync requesting permissions.
*/
fun requestPermissions() {
Logger.i("Requesting all required permissions")
requestLocationPermission()
requestNearbyPermission()
}
/**
* Async requesting permissions.
*/
suspend fun requestPermissionsAsync(): CompletableFuture<Boolean> {
val res = checkPermissions()
if (res) return CompletableFuture.completedFuture(true)
Logger.i("Requesting all required permissions")
return CompletableFuture.completedFuture(
requestLocationPermission().await() && requestNearbyPermission().await()
)
}
/**
* Requesting location permission [Manifest.permission.ACCESS_FINE_LOCATION]
*/
private fun requestLocationPermission(): CompletableFuture<Boolean> {
if (checkLocationPermission()) {
return CompletableFuture.completedFuture(true)
}
activity?.requestPermissions(
arrayOf(locationPermission.name), locationPermission.code
)
return locationPermission.future
}
/**
* Requesting nearby devices permission [Manifest.permission.NEARBY_WIFI_DEVICES].
* Calls if [Build.VERSION.SDK_INT] is equal or more than [Build.VERSION_CODES.TIRAMISU]
*/
private fun requestNearbyPermission(): CompletableFuture<Boolean> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (checkNearbyPermission()) {
return CompletableFuture.completedFuture(true)
}
activity?.requestPermissions(
arrayOf(nearbyPermission.name), nearbyPermission.code
)
return nearbyPermission.future
} else {
return CompletableFuture.completedFuture(true)
}
}
/**
* Checking location permission [Manifest.permission.ACCESS_FINE_LOCATION]
*/
private fun checkLocationPermission(): Boolean {
return context.checkSelfPermission(
locationPermission.name
) == PackageManager.PERMISSION_GRANTED
}
/**
* Checking nearby devices permission [Manifest.permission.NEARBY_WIFI_DEVICES].
* Available if [Build.VERSION.SDK_INT] is equal or more than [Build.VERSION_CODES.TIRAMISU]
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun checkNearbyPermission(): Boolean {
return context.checkSelfPermission(
nearbyPermission.name
) == PackageManager.PERMISSION_GRANTED
}
/**
* Permission result handler.
* Completes the future of permission with the provided [requestCode] if it was granted.
*/
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray
): Boolean {
if (grantResults.isNotEmpty()) {
if (requestCode == locationPermission.code) {
val isGranted = checkLocationPermission()
locationPermission.future.complete(isGranted)
Logger.i("Location permission activity result: isGranted=$isGranted")
return isGranted
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && requestCode == nearbyPermission.code) {
val isGranted = checkNearbyPermission()
nearbyPermission.future.complete(isGranted)
Logger.i("Nearby devices permission activity result: isGranted=$isGranted")
return isGranted
}
}
return false
}
}
@@ -0,0 +1,185 @@
package com.xenikii.nearby_service
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
const val CHANNEL_NAME = "nearby_service"
const val PEERS_CHANNEL_NAME = "nearby_service_peers"
const val CONNECTED_DEVICE_CHANNEL_NAME = "nearby_service_connected_device"
/**
* Plugin for creating connections in the Wi-fi Direct scope.
*/
class NearbyServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var binaryMessenger: BinaryMessenger
private lateinit var channel: MethodChannel
private lateinit var manager: NearbyServiceManager
private lateinit var peersChannel: EventChannel
private lateinit var connectedDeviceChannel: EventChannel
@OptIn(DelicateCoroutinesApi::class)
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
}
"getPlatformModel" -> {
result.success(android.os.Build.MODEL)
}
"initialize" -> {
try {
manager.initialize(result, call.argument("logLevel") ?: "DEBUG")
} catch (e: Exception) {
onError(result, e)
}
}
"requestPermissions" -> {
GlobalScope.launch {
try {
result.success(manager.requestPermissions())
} catch (e: Exception) {
onError(result, e)
}
}
}
"getCurrentDevice" -> {
try {
manager.getCurrentDevice(result)
} catch (e: Exception) {
onError(result, e)
}
}
"checkWifiService" -> {
try {
manager.checkWifiService(result)
} catch (e: Exception) {
onError(result, e)
}
}
"openServicesSettings" -> {
try {
manager.openWifiSettings(result)
} catch (e: Exception) {
onError(result, e)
}
}
"discover" -> {
try {
manager.discover(result)
} catch (e: Exception) {
onError(result, e)
}
}
"stopDiscovery" -> {
try {
manager.stopDiscovery(result)
} catch (e: Exception) {
onError(result, e)
}
}
"getPeers" -> {
try {
manager.getPeers(result)
} catch (e: Exception) {
onError(result, e)
}
}
"getConnectionInfo" -> {
try {
manager.getConnectionInfo(result)
} catch (e: Exception) {
onError(result, e)
}
}
"connect" -> {
try {
manager.connect(result, call.argument("deviceAddress") ?: "")
} catch (e: Exception) {
onError(result, e)
}
}
"disconnect" -> {
try {
manager.disconnect(result)
} catch (e: Exception) {
onError(result, e)
}
}
else -> {
result.notImplemented()
}
}
}
private fun onError(result: Result, e: Exception) {
Logger.e(e.message.toString())
result.success(false)
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
binaryMessenger = flutterPluginBinding.binaryMessenger
manager = NearbyServiceManager(flutterPluginBinding.applicationContext)
channel = MethodChannel(binaryMessenger, CHANNEL_NAME)
channel.setMethodCallHandler(this)
peersChannel = EventChannel(binaryMessenger, PEERS_CHANNEL_NAME)
peersChannel.setStreamHandler(manager.peersHandler)
connectedDeviceChannel = EventChannel(binaryMessenger, CONNECTED_DEVICE_CHANNEL_NAME)
connectedDeviceChannel.setStreamHandler(manager.connectedDeviceInfoHandler)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
peersChannel.setStreamHandler(null)
connectedDeviceChannel.setStreamHandler(null)
manager.disconnect()
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
manager.setBinding(binding)
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
manager.setBinding(binding)
}
override fun onDetachedFromActivityForConfigChanges() {
manager.removeBinding()
}
override fun onDetachedFromActivity() {
manager.removeBinding()
}
}
+46
View File
@@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
ios/Podfile.lock
+16
View File
@@ -0,0 +1,16 @@
# nearby_service_example
Demonstrates how to use the nearby_service plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+29
View File
@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+13
View File
@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks
+72
View File
@@ -0,0 +1,72 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
namespace "com.xenikii.nearby_service_example"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.xenikii.nearby_service_example"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 24
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<application
android:label="nearby_service_example"
android:name="${applicationName}"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
@@ -0,0 +1,6 @@
package com.xenikii.nearby_service_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+31
View File
@@ -0,0 +1,31 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
+3
View File
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
+11
View File
@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>11.0</string>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+44
View File
@@ -0,0 +1,44 @@
# Uncomment this line to define a global platform for your project
platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
@@ -0,0 +1,730 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
105AA12BAD8DD4B2A812DBD2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 624D99DEA58F3E8BD42B371A /* Pods_Runner.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
8648A20CA5045AFB8BB3E363 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9E0D8D755156697FC0D0C80 /* Pods_RunnerTests.framework */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
05CF70028C0A81E25BD55790 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
26D9CFAD8440F20A9E463633 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
3D2190ED2980EDDA878B3F59 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
624D99DEA58F3E8BD42B371A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9620C2F9D759E6071BD1FD4C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B3749AC35EBBA44996820B50 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
E8C7D709D778AD6A303C6754 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
F9E0D8D755156697FC0D0C80 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
0CF42BA1FAAFA371D083248F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
8648A20CA5045AFB8BB3E363 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
105AA12BAD8DD4B2A812DBD2 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
16C54B649C90DF9832FFDAE8 /* Frameworks */ = {
isa = PBXGroup;
children = (
624D99DEA58F3E8BD42B371A /* Pods_Runner.framework */,
F9E0D8D755156697FC0D0C80 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
D83928EFDF10A002814333D0 /* Pods */,
16C54B649C90DF9832FFDAE8 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
D83928EFDF10A002814333D0 /* Pods */ = {
isa = PBXGroup;
children = (
05CF70028C0A81E25BD55790 /* Pods-Runner.debug.xcconfig */,
9620C2F9D759E6071BD1FD4C /* Pods-Runner.release.xcconfig */,
26D9CFAD8440F20A9E463633 /* Pods-Runner.profile.xcconfig */,
E8C7D709D778AD6A303C6754 /* Pods-RunnerTests.debug.xcconfig */,
3D2190ED2980EDDA878B3F59 /* Pods-RunnerTests.release.xcconfig */,
B3749AC35EBBA44996820B50 /* Pods-RunnerTests.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
52DB770C136B76078924CF59 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
0CF42BA1FAAFA371D083248F /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
65E91E1B2FC1A3C9E222C4B4 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
7E005F5F8255249BA748FE33 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
52DB770C136B76078924CF59 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
65E91E1B2FC1A3C9E222C4B4 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
7E005F5F8255249BA748FE33 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.xenikii.nearbyServiceExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = E8C7D709D778AD6A303C6754 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.xenikii.nearbyServiceExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 3D2190ED2980EDDA878B3F59 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.xenikii.nearbyServiceExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = B3749AC35EBBA44996820B50 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.xenikii.nearbyServiceExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.xenikii.nearbyServiceExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.xenikii.nearbyServiceExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+13
View File
@@ -0,0 +1,13 @@
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="79" y="-34"/>
</scene>
</scenes>
</document>
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Nearby Service</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>nearby_service_example</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>NSBonjourServices</key>
<array>
<string>_mp-connection._tcp</string>
</array>
<key>UIRequiresPersistentWiFi</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+26
View File
@@ -0,0 +1,26 @@
import Flutter
import UIKit
import XCTest
@testable import nearby_service
// This demonstrates a simple unit test of the Swift portion of this plugin's implementation.
//
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
class RunnerTests: XCTestCase {
func testGetPlatformVersion() {
let plugin = NearbyServicePlugin()
let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: [])
let resultExpectation = expectation(description: "result block must be called.")
plugin.handle(call) { result in
XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion)
resultExpectation.fulfill()
}
waitForExpectations(timeout: 1)
}
}
+45
View File
@@ -0,0 +1,45 @@
part of '../main.dart';
enum _ActionButtonType {
idle(Color(0xFF00C853)),
warning(Color(0xFFD50000));
const _ActionButtonType(this.color);
final Color color;
}
class _ActionButton extends StatelessWidget {
const _ActionButton({
required this.onTap,
required this.title,
this.type = _ActionButtonType.idle,
});
final VoidCallback onTap;
final String title;
final _ActionButtonType type;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onTap,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.white,
maximumSize: const Size(150, 70),
minimumSize: const Size(70, 70),
elevation: 3,
surfaceTintColor: type.color.withOpacity(0.05),
),
child: Text(
title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: type.color,
height: 1,
),
),
);
}
}
+40
View File
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
class AppShackBar {
AppShackBar._();
static ScaffoldFeatureController<SnackBar, SnackBarClosedReason>? show(
BuildContext context,
String title, {
String? subtitle,
}) {
return ScaffoldMessenger.maybeOf(context)?.showSnackBar(
SnackBar(
content: RichText(
text: TextSpan(
text: '$title \n',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
children: [
if (subtitle != null)
TextSpan(
text: subtitle,
style: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
),
],
),
),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.only(
left: 12,
right: 12,
bottom: 20,
),
backgroundColor: Colors.pink.shade800,
duration: const Duration(seconds: 2),
),
);
}
}
+931
View File
@@ -0,0 +1,931 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:nearby_service/nearby_service.dart';
import 'package:provider/provider.dart';
import 'components/app_snack_bar.dart';
part 'components/action_button.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final service = AppService();
await service.getPlatformInfo();
runApp(
MyApp(service: service),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key, required this.service});
final AppService service;
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: service,
child: MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink),
),
home: Scaffold(
appBar: AppBar(
title: const Text('Nearby service example app'),
),
body: Consumer<AppService>(builder: (context, service, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Platform: ${service.platformVersion}\n'
'Model: ${service.platformModel}',
),
if (service.currentDevice != null)
Text(
'Device Name: ${service.currentDevice!.displayName}\n'
'${Platform.isIOS ? 'Device ID: ${service.currentDevice!.id}' : ''}\n',
),
if (Platform.isIOS)
Text(
'You are ${service.isIOSBrowser ? 'going to find your friend' : 'waiting for another user to connect'}\n',
),
],
),
),
Flexible(
child: MediaQuery.removePadding(
context: context,
removeLeft: true,
child: Stepper(
controlsBuilder: (context, _) => const SizedBox.shrink(),
currentStep: service.state.step,
steps: [
...AppState.steps.map((e) {
return Step(
title: Text(
e.title,
style: const TextStyle(fontSize: 14),
),
subtitle:
e.subtitle != null ? Text(e.subtitle!) : null,
content: e.content,
isActive: e == service.state,
);
}),
],
),
),
),
],
);
}),
),
),
);
}
}
///
/// DOMAIN LEVEL
///
enum AppState {
idle(title: "Let's start!"),
permissions(title: "Provide permissions"),
checkServices(title: "Check services"),
selectClientType(
title: 'Do you want to find your friend from this device?',
subtitle:
'Click "Yes" if you will search, click "No" if you will wait for your friend to connect',
),
readyToDiscover(title: "Ready to discover!"),
discoveringPeers(title: "Discovering devices..."),
streamingPeers(title: "Peers stream got!"),
loadingConnection(title: "Loading your connection"),
connected(title: "Connected!"),
communicationChannelCreated(title: "You can communicate!");
static final List<AppState> androidSteps = [
AppState.idle,
AppState.permissions,
AppState.checkServices,
AppState.readyToDiscover,
AppState.discoveringPeers,
AppState.streamingPeers,
AppState.loadingConnection,
AppState.connected,
AppState.communicationChannelCreated,
];
static final List<AppState> iosSteps = [
AppState.idle,
AppState.selectClientType,
AppState.readyToDiscover,
AppState.discoveringPeers,
AppState.streamingPeers,
AppState.loadingConnection,
AppState.connected,
AppState.communicationChannelCreated,
];
static final List<AppState> steps = [
if (Platform.isAndroid) ...androidSteps,
if (Platform.isIOS) ...iosSteps,
];
const AppState({required this.title, this.subtitle});
final String title;
final String? subtitle;
Widget get content {
return switch (this) {
(AppState.idle) => const _IdleBody(),
(AppState.permissions) => const _PermissionsBody(),
(AppState.checkServices) => const _CheckServiceBody(),
(AppState.selectClientType) => const _SelectClientTypeBody(),
(AppState.readyToDiscover) => const _ReadyBody(),
(AppState.discoveringPeers) => const _DiscoveringBody(),
(AppState.streamingPeers) => const _StreamingState(),
(AppState.loadingConnection) =>
const Center(child: CircularProgressIndicator.adaptive()),
(AppState.connected) => const _ConnectedBody(),
(AppState.communicationChannelCreated) => const _ConnectedSocketBody(),
};
}
int get step {
return steps.indexOf(this);
}
}
class AppService extends ChangeNotifier {
final _nearbyService = NearbyService.getInstance();
AppState state = AppState.idle;
List<NearbyDevice>? peers;
NearbyDevice? connectedDevice;
NearbyDeviceInfo? currentDevice;
String platformVersion = 'Unknown';
String platformModel = 'Unknown';
StreamSubscription? peersSubscription;
StreamSubscription? connectedDeviceSubscription;
bool get isCommunicationChannelConnecting {
return _nearbyService.isCommunicationChannelConnecting.value;
}
bool get isIOSBrowser {
return _nearbyService.ios?.isBrowser.value ?? false;
}
bool get isAndroidGroupOwner {
return Platform.isAndroid &&
(_nearbyService.android?.connectionInfo?.isGroupOwner ?? false);
}
Future<void> getPlatformInfo() async {
platformVersion = await _nearbyService.getPlatformVersion() ?? 'Unknown';
platformModel = await _nearbyService.getPlatformModel() ?? 'Unknown';
notifyListeners();
}
Future<String> getSavedDeviceName() async {
return (await _nearbyService.ios?.getSavedDeviceName()) ?? platformModel;
}
Future<void> initialize(String? iosDeviceName) async {
try {
await _nearbyService.initialize(
data: NearbyInitializeData(iosDeviceName: iosDeviceName),
);
currentDevice = (await _nearbyService.getCurrentDevice())?.info;
updateState(
Platform.isAndroid ? AppState.permissions : AppState.selectClientType,
);
} catch (e, s) {
if (kDebugMode) {
print(e);
print(s);
}
} finally {
notifyListeners();
}
}
Future<void> requestPermissions() async {
try {
final result = await _nearbyService.android?.requestPermissions();
if (result ?? false) {
updateState(AppState.checkServices);
}
} catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<bool> checkWifiService() async {
final result = await _nearbyService.android?.checkWifiService();
if (result ?? false) {
updateState(AppState.readyToDiscover);
return true;
}
return false;
}
Future<void> openServicesSettings() async {
await _nearbyService.openServicesSettings();
}
void setIsBrowser({required bool value}) {
_nearbyService.ios?.setIsBrowser(value: value);
updateState(AppState.readyToDiscover);
}
Future<void> discover() async {
try {
final result = await _nearbyService.discover();
if (result) {
updateState(AppState.discoveringPeers);
}
} catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<void> stopDiscovery() async {
try {
final result = await _nearbyService.stopDiscovery();
if (result) {
updateState(AppState.readyToDiscover);
}
} catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<void> startListeningPeers() async {
try {
peersSubscription = _nearbyService.getPeersStream().listen(
(event) {
peers = event;
notifyListeners();
},
);
updateState(AppState.streamingPeers);
} catch (e) {
if (kDebugMode) {
print(e);
}
}
}
Future<void> stopListeningPeers() async {
peersSubscription?.cancel();
peers = null;
updateState(AppState.discoveringPeers);
}
Future<void> connect(NearbyDevice device) async {
try {
await _nearbyService.connect(device);
} catch (e) {
if (kDebugMode) {
print(e);
}
}
notifyListeners();
}
Future<void> startListeningConnectedDevice(NearbyDevice device) async {
updateState(AppState.loadingConnection);
try {
connectedDeviceSubscription =
_nearbyService.getConnectedDeviceStream(device).listen(
(event) async {
final wasConnected = connectedDevice?.status.isConnected ?? false;
final nowConnected = event?.status.isConnected ?? false;
if (wasConnected && !nowConnected) {
restart();
return;
}
connectedDevice = event;
if (connectedDevice != null &&
state != AppState.connected &&
state != AppState.communicationChannelCreated) {
updateState(AppState.connected);
}
notifyListeners();
},
);
} catch (e) {
updateState(AppState.streamingPeers, shouldNotify: false);
}
notifyListeners();
}
Future<void> stopListeningConnectedDevice() async {
await connectedDeviceSubscription?.cancel();
await _nearbyService.endCommunicationChannel();
connectedDeviceSubscription = null;
connectedDevice = null;
notifyListeners();
}
Future<void> startCommunicationChannel({
ValueChanged<ReceivedNearbyMessage>? listener,
}) async {
final eventListener = NearbyServiceStreamListener<ReceivedNearbyMessage>(
onCreated: (_) {
updateState(AppState.communicationChannelCreated);
},
onData: (event) {
listener?.call(event);
},
onError: (e, [StackTrace? s]) {
restart();
},
);
await _nearbyService.startCommunicationChannel(
NearbyCommunicationChannelData(
connectedDevice!.info.id,
eventListener: eventListener,
),
);
}
void send(String message) {
if (connectedDevice == null) return;
_nearbyService.send(
OutgoingNearbyMessage(
value: message,
receiver: connectedDevice!.info,
),
);
}
Future<void> disconnect(NearbyDevice device) async {
try {
await _nearbyService.disconnect(device);
} catch (e) {
if (kDebugMode) {
print(e);
}
} finally {
await restart();
}
notifyListeners();
}
Future<void> restart() async {
await stopListeningConnectedDevice();
await stopListeningPeers();
await stopDiscovery();
await discover();
}
void updateState(AppState state, {bool shouldNotify = true}) {
this.state = state;
if (shouldNotify) {
notifyListeners();
}
}
}
///
/// APP STATES
///
///
class _IdleBody extends StatefulWidget {
const _IdleBody();
@override
State<_IdleBody> createState() => _IdleBodyState();
}
class _IdleBodyState extends State<_IdleBody> {
late final controller = TextEditingController();
bool initialized = false;
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
context.read<AppService>().getSavedDeviceName().then((value) {
controller.text = value;
controller.selection = TextSelection.collapsed(offset: value.length);
setState(() {
initialized = true;
});
});
});
super.initState();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!initialized) {
return const Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Getting saved name...',
style: TextStyle(fontSize: 12),
),
SizedBox(height: 10),
CircularProgressIndicator.adaptive(),
],
),
);
}
return Center(
child: Column(
children: [
if (Platform.isIOS)
Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: TextFormField(
decoration: const InputDecoration(
labelText: 'Device Name',
hintText: 'Enter the name of your device',
),
controller: controller,
),
),
_ActionButton(
onTap: () {
context.read<AppService>().initialize(controller.text);
},
title: 'Tap to start',
),
],
),
);
}
}
class _PermissionsBody extends StatelessWidget {
const _PermissionsBody();
@override
Widget build(BuildContext context) {
return Consumer<AppService>(builder: (context, service, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_ActionButton(
onTap: service.requestPermissions,
title: 'Request permissions',
),
],
);
});
}
}
class _CheckServiceBody extends StatefulWidget {
const _CheckServiceBody();
@override
State<_CheckServiceBody> createState() => _CheckServiceBodyState();
}
class _CheckServiceBodyState extends State<_CheckServiceBody> {
bool showEnableButton = false;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_ActionButton(
onTap: () {
context.read<AppService>().checkWifiService().then((value) {
if (!value) {
setState(() {
showEnableButton = true;
});
AppShackBar.show(context, 'Please enable Wi-fi');
}
});
},
title: 'Check Wi-fi service',
),
if (showEnableButton)
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: _ActionButton(
onTap: context.read<AppService>().openServicesSettings,
title: 'Open settings',
),
),
],
);
}
}
class _SelectClientTypeBody extends StatelessWidget {
const _SelectClientTypeBody();
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_ActionButton(
title: 'Yes',
onTap: () {
context.read<AppService>().setIsBrowser(value: true);
},
),
const SizedBox(width: 10),
_ActionButton(
title: 'No',
onTap: () {
context.read<AppService>().setIsBrowser(value: false);
},
),
],
);
}
}
class _ReadyBody extends StatelessWidget {
const _ReadyBody();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_ActionButton(
onTap: context.read<AppService>().discover,
title: 'Start discover peers',
),
const SizedBox(height: 10),
if (Platform.isIOS)
_ActionButton(
onTap: () {
context.read<AppService>().updateState(AppState.selectClientType);
},
title: 'Reselect client type',
type: _ActionButtonType.warning,
),
],
);
}
}
class _DiscoveringBody extends StatelessWidget {
const _DiscoveringBody();
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_ActionButton(
type: _ActionButtonType.warning,
onTap: context.read<AppService>().stopDiscovery,
title: 'Stop discovery',
),
const SizedBox(height: 10),
_ActionButton(
onTap: context.read<AppService>().startListeningPeers,
title: 'Now it is discovering. Tap to get peers!',
)
],
);
}
}
class _StreamingState extends StatelessWidget {
const _StreamingState();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_ActionButton(
type: _ActionButtonType.warning,
onTap: context.read<AppService>().stopListeningPeers,
title: 'Stop stream peers',
),
const SizedBox(height: 10),
const _PeersBody(),
],
);
}
}
class _PeersBody extends StatelessWidget {
const _PeersBody();
@override
Widget build(BuildContext context) {
return Consumer<AppService>(
builder: (context, service, _) {
return (service.peers != null && service.peers!.isNotEmpty)
? Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
...service.peers!.map(
(e) {
return Container(
padding: const EdgeInsets.only(bottom: 8.0),
child: _DevicePreview(device: e),
);
},
),
],
)
: Text(
Platform.isAndroid || service.isIOSBrowser
? 'No one here ('
: "Wait until someone invites you!",
textAlign: TextAlign.center,
);
},
);
}
}
class _ConnectedBody extends StatelessWidget {
const _ConnectedBody();
@override
Widget build(BuildContext context) {
return Consumer<AppService>(
builder: (context, service, _) {
final device = service.connectedDevice;
return device != null
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (!device.status.isConnected)
const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text('Connection lost'),
),
)
else if (!device.status.isConnected)
Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Connection lost'),
_ActionButton(
onTap: () {
service.connect(device);
},
title: 'Reconnect',
),
],
)
else
_DevicePreview(device: device, largeView: true),
const SizedBox(height: 10),
if (!(service.isCommunicationChannelConnecting))
_ActionButton(
onTap: () => service.startCommunicationChannel(
listener: (event) => AppShackBar.show(
Scaffold.of(context).context,
event.value,
subtitle: 'From ${event.sender.displayName} '
'(ID: ${event.sender.id})',
),
),
title: 'Start communicate',
)
else
Text(
'Connecting socket.. '
'${service.isAndroidGroupOwner ? "Waiting a client for connect" : "Waiting a connection"}',
)
],
),
)
: const SizedBox();
},
);
}
}
class _ConnectedSocketBody extends StatefulWidget {
const _ConnectedSocketBody();
@override
State<_ConnectedSocketBody> createState() => _ConnectedSocketBodyState();
}
class _ConnectedSocketBodyState extends State<_ConnectedSocketBody> {
String message = '';
@override
Widget build(BuildContext context) {
return Consumer<AppService>(
builder: (context, service, _) {
final device = service.connectedDevice;
if (device == null) {
return Center(
child: _ActionButton(
onTap: service.restart,
title: 'Restart',
),
);
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_DevicePreview(device: device, largeView: true),
const SizedBox(height: 10),
Flexible(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: TextField(
onChanged: (value) => setState(() {
message = value;
}),
decoration: const InputDecoration(
enabledBorder: OutlineInputBorder(),
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(),
hintText: 'Enter your message',
),
),
),
const SizedBox(width: 10),
_ActionButton(
title: 'Send',
onTap: () {
service.send(message);
},
),
],
),
),
],
);
},
);
}
}
class _DevicePreview extends StatelessWidget {
const _DevicePreview({required this.device, this.largeView = false});
final NearbyDevice device;
final bool largeView;
@override
Widget build(BuildContext context) {
final color = device.status.isConnected
? Colors.greenAccent.shade700
: Colors.blueGrey;
final avatar = CircleAvatar(
backgroundColor: Colors.pink.shade800,
foregroundColor: Colors.white,
maxRadius: largeView ? 100 : 30,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
device.info.displayName
.split(' ')
.map((e) => e.substring(0, 1).toUpperCase())
.join(''),
style: TextStyle(fontSize: largeView ? 32 : 16),
),
),
);
final name = Text(
'${device.info.displayName} '
'${device.byPlatform(onAndroid: (d) => d.isGroupOwner ? " - group owner" : "") ?? ''}',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
);
final id = Text(
'ID: ${device.info.id}',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
fontSize: 10,
),
);
final status = Text(
device.byPlatform(
onAny: (d) => d.status.name,
onIOS: (d) =>
context.select<AppService, bool>((v) => v.isIOSBrowser)
? "Peer found | ${d.status.name}"
: "Pending invitation | ${d.status.name}",
) ??
'',
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: color,
),
);
final disconnectButton = device.status.isConnected
? TextButton(
onPressed: () => context.read<AppService>().disconnect(device),
child: const Text('Disconnect'),
)
: null;
if (largeView) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
avatar,
const SizedBox(height: 10),
name,
const SizedBox(height: 5),
id,
if (disconnectButton != null)
Padding(
padding: const EdgeInsets.only(top: 10),
child: disconnectButton,
),
],
);
} else {
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
if (!device.status.isConnected) {
context.read<AppService>().connect(device);
} else {
context.read<AppService>().startListeningConnectedDevice(device);
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 3,
child: Row(
children: [
avatar,
const SizedBox(width: 10),
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [name, id, status],
),
),
],
),
),
Flexible(
child: Text(
device.status.isConnected ? 'Tap to chat' : 'Tap to connect',
style: TextStyle(
color: Colors.greenAccent.shade700, fontSize: 12),
textAlign: TextAlign.center,
),
)
],
),
),
);
}
}
}
+26
View File
@@ -0,0 +1,26 @@
name: nearby_service_example
description: Demonstrates how to use the nearby_service plugin.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: '>=3.0.6 <4.0.0'
dependencies:
flutter:
sdk: flutter
nearby_service:
path: ../
permission_handler: ^11.0.1
provider: ^6.1.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
flutter:
uses-material-design: true
+29
View File
@@ -0,0 +1,29 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nearby_service_example/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp(
service: AppService(),
));
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) =>
widget is Text && widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}
+38
View File
@@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh
View File
@@ -0,0 +1,35 @@
import Flutter
class ConnectedDeviceStreamHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private var timer : Timer?
func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
let deviceId = arguments as! String
self.eventSink = eventSink
startSendingUpdates(deviceId: deviceId)
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
stopSendingUpdates()
eventSink = nil
return nil
}
func startSendingUpdates(deviceId:String) {
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
var result: String?
if let device = NearbyDevicesStore.instance.find(for: deviceId) {
result = device.toJsonString()
}
self.eventSink?(result)
}
}
func stopSendingUpdates() {
self.timer?.invalidate()
self.timer = nil
}
}
+15
View File
@@ -0,0 +1,15 @@
//
// Logger.swift
// nearby_service
//
// Created by Kseniia Nikitina on 26/1/24.
//
import Foundation
class Logger {
static func error(message: String) {
NSLog("NearbyServicePluginError -- %@", message)
}
}
+90
View File
@@ -0,0 +1,90 @@
import Foundation
import MultipeerConnectivity
let SERVICE_TYPE = "mp-connection"
let PEER_ID = "PEER-ID"
let DEVICE_NAME = "DEVICE-NAME"
class MyDeviceDataGenerator {
static func generate(name: String?) -> NearbyDevice {
return NearbyDevice(
peerID: ArchivedData.getPeerID(),
name: ArchivedData.getRequireName(from: name),
deviceType: UIDevice.current.model,
os: UIDevice.current.systemName,
osVersion: UIDevice.current.systemVersion
)
}
}
class ArchivedData {
static func getPeerID() -> MCPeerID {
if let archivedPeerID = getArchivedPeerID() {
return archivedPeerID
} else {
let peerID = MCPeerID(
displayName: UIDevice.current.name.replacingOccurrences(of: " ", with: "_")
+ "_"
+ String(Int.random(in: 1000..<50000))
)
savePeerID(for: peerID)
return peerID
}
}
static func getRequireName(from name: String?) -> String {
let archivedName = ArchivedData.getArchivedName()
if let newName = name {
if (archivedName != newName) {
ArchivedData.saveName(for: newName)
}
return newName
}
return archivedName ?? UIDevice.current.name
}
static func getArchivedPeerID() -> MCPeerID? {
guard let savedData = UserDefaults.standard.data(forKey: PEER_ID),
let unarchivedPeerID = try? NSKeyedUnarchiver.unarchivedObject(ofClass: MCPeerID.self, from: savedData)
else {
return nil
}
return unarchivedPeerID
}
static func savePeerID(for peerID: MCPeerID) {
do {
UserDefaults.standard.set(
try NSKeyedArchiver.archivedData(withRootObject: peerID, requiringSecureCoding: false),
forKey: PEER_ID
)
} catch let e {
Logger.error(message: e.localizedDescription)
}
}
static func getArchivedName() -> String? {
guard let savedData = UserDefaults.standard.data(forKey: DEVICE_NAME),
let unarchivedName = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSData.self, from: savedData)
else {
return nil
}
return String(data: unarchivedName as Data, encoding: .utf8)
}
static func saveName(for name: String) {
if let data = name.data(using: .utf8) {
do {
UserDefaults.standard.set(
try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: false),
forKey: DEVICE_NAME
)
} catch let e {
Logger.error(message: e.localizedDescription)
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
import Foundation
import MultipeerConnectivity
class NearbyDevice : NSObject {
var name: String
var peerID: MCPeerID
var deviceType: String?
var os: String?
var osVersion: String?
var session: NearbySession?
init(
peerID: MCPeerID,
name:String,
deviceType:String? = nil,
os: String? = nil,
osVersion: String? = nil
) {
self.name=name
self.peerID = peerID
self.deviceType=deviceType
self.os=os
self.osVersion=osVersion
}
func createSession(for peerID: MCPeerID) -> NearbySession {
self.session = NearbySession.create(peerID: peerID)
return session!
}
func deleteSession() {
self.session = nil
}
}
extension NearbyDevice {
static func fromDictionary(for dictionary: [String: String]?, with peerID: MCPeerID) -> NearbyDevice {
let device = NearbyDevice(
peerID: peerID,
name:(dictionary?["displayName"] as String?) ?? peerID.displayName,
deviceType: dictionary?["deviceType"] as String?,
os: dictionary?["os"] as String?,
osVersion: dictionary?["osVersion"] as String?
)
return device
}
func toDictionary() -> [String: String] {
var deviceDict: [String: String] = [
"displayName": self.name,
"id": self.peerID.displayName,
]
if let os = self.os {
deviceDict["os"] = os
}
if let deviceType = self.deviceType {
deviceDict["deviceType"] = deviceType
}
if let osVersion = self.osVersion {
deviceDict["osVersion"] = osVersion
}
if let session = self.session {
deviceDict["state"] = String(session.state.rawValue)
} else {
deviceDict["state"] = String(0)
}
return deviceDict
}
func toJsonString() -> String? {
var result: String?
do {
let jsonData = try JSONSerialization.data(withJSONObject: toDictionary())
if let jsonString = String(data: jsonData, encoding: .utf8) {
result = jsonString
}
} catch let error {
Logger.error(message: error.localizedDescription)
}
return result
}
}
+58
View File
@@ -0,0 +1,58 @@
//
// NearbyDevicesStore.swift
// nearby_service
//
// Created by Kseniia Nikitina on 16/1/24.
//
import Foundation
import MultipeerConnectivity
class NearbyDevicesStore : NSObject {
static let instance = NearbyDevicesStore()
private var devices : [NearbyDevice] = []
func getDevices() -> [NearbyDevice] {
return devices
}
func find(for deviceId: String) -> NearbyDevice? {
return devices.first { device in
return device.peerID.displayName == deviceId
}
}
func getDevicesToJsonString() -> String {
let devicesObject = devices.map { device in
return device.toDictionary()
}
do {
let jsonData = try JSONSerialization.data(withJSONObject: devicesObject)
if let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
} catch {
return "[]"
}
return "[]"
}
func add(for peerID: MCPeerID, discoveryInfo: [String: String]? = nil) -> NearbyDevice? {
devices = devices.filter{$0.peerID.displayName != peerID.displayName}
let device = NearbyDevice.fromDictionary(for: discoveryInfo, with: peerID)
self.devices.append(device)
return device
}
func remove(for peerID: MCPeerID) {
self.devices = devices.filter{$0.peerID.displayName != peerID.displayName}
}
func clear() {
self.devices = []
}
}
+154
View File
@@ -0,0 +1,154 @@
import Foundation
import Flutter
import MultipeerConnectivity
class NearbyManager: NSObject {
var device: NearbyDevice!
var advertiser: MCNearbyServiceAdvertiser!
var browser: MCNearbyServiceBrowser!
var invitationHandlers: [String: ((Bool, MCSession?) -> Void)] = [:]
func initialize(for deviceName: String? = nil, result: @escaping FlutterResult) {
self.device = MyDeviceDataGenerator.generate(name: deviceName)
self.advertiser = MCNearbyServiceAdvertiser(
peer: self.device.peerID,
discoveryInfo: self.device.toDictionary(),
serviceType: SERVICE_TYPE
)
self.advertiser.delegate = self
self.browser = MCNearbyServiceBrowser(peer: self.device.peerID, serviceType: SERVICE_TYPE)
self.browser.delegate = self
result(true)
}
func getSavedDeviceName(result: @escaping FlutterResult) {
result(ArchivedData.getArchivedName())
}
func getCurrentDevice(result: @escaping FlutterResult) {
result(device.toJsonString())
}
func openServicesSettings(result: @escaping FlutterResult) {
if let url = URL(string:UIApplication.openSettingsURLString) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
result(true)
}
func startAdvertising(result: @escaping FlutterResult) {
self.advertiser.startAdvertisingPeer()
result(true)
}
func startBrowsing(result: @escaping FlutterResult) {
self.browser.startBrowsingForPeers()
result(true)
}
func stopAdvertising(result: @escaping FlutterResult) {
self.advertiser.stopAdvertisingPeer()
NearbyDevicesStore.instance.clear()
result(true)
}
func stopBrowsing(result: @escaping FlutterResult) {
self.browser.stopBrowsingForPeers()
NearbyDevicesStore.instance.clear()
result(true)
}
func getPeers(result: @escaping FlutterResult) {
result(NearbyDevicesStore.instance.getDevicesToJsonString())
}
func invite(for deviceId: String, result: @escaping FlutterResult) {
do {
let device = NearbyDevicesStore.instance.find(for: deviceId)
if let requireDevice = device {
let nearbySession = requireDevice.createSession(for: self.device.peerID)
self.browser.invitePeer(
requireDevice.peerID,
to: nearbySession.session,
withContext: try JSONSerialization.data(withJSONObject:["displayName": self.device.name]),
timeout: 0
)
result(true)
}
} catch let error {
Logger.error(message: error.localizedDescription)
result(false)
}
}
func acceptInvite(for deviceId: String, result: @escaping FlutterResult) {
let device = NearbyDevicesStore.instance.find(for: deviceId)
if let requireDevice = device {
let nearbySession = requireDevice.createSession(for: self.device.peerID)
self.invitationHandlers[deviceId]?(true, nearbySession.session)
result(true)
}
}
func disconnect(for deviceId: String, result: @escaping FlutterResult) {
let device = NearbyDevicesStore.instance.find(for: deviceId)
device?.deleteSession()
result(true)
}
func send(for message: String, with receiverId: String, result: @escaping FlutterResult) {
let device = NearbyDevicesStore.instance.find(for: receiverId)
do {
if let requireDevice = device {
let data = [
"name": self.device.name,
"message": message
]
try requireDevice.session?.session?.send(
try JSONSerialization.data(withJSONObject: data),
toPeers: [requireDevice.peerID],
with: MCSessionSendDataMode.reliable
)
}
} catch let error {
Logger.error(message: error.localizedDescription)
}
result(true)
}
}
extension NearbyManager: MCNearbyServiceAdvertiserDelegate {
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void
) {
var dict: Dictionary<String, String>?
do {
dict = try JSONSerialization.jsonObject(with: context ?? Data()) as? Dictionary<String, String>
} catch let error {
Logger.error(message: error.localizedDescription)
}
_ = NearbyDevicesStore.instance.add(for: peerID, discoveryInfo: dict)
self.invitationHandlers[peerID.displayName] = invitationHandler
}
}
extension NearbyManager: MCNearbyServiceBrowserDelegate {
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
_ = NearbyDevicesStore.instance.add(for: peerID, discoveryInfo: info)
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
let device = NearbyDevicesStore.instance.find(for: peerID.displayName)
device?.deleteSession()
NearbyDevicesStore.instance.remove(for: peerID)
}
}
+59
View File
@@ -0,0 +1,59 @@
//
// NearbyMessageConverter.swift
// nearby_service
//
// Created by Kseniia Nikitina on 29/1/24.
//
import Foundation
import MultipeerConnectivity
class NearbyMessageConverter {
static func convert(userInfo: [AnyHashable : Any]?) -> String? {
if let data = getMessageData(userInfo: userInfo),
let peerID = getMessagePeerID(userInfo: userInfo) {
return createMessage(data: data, peerID: peerID)
}
return nil
}
static private func getMessageData(userInfo: [AnyHashable : Any]?) -> [String: String]? {
do {
if let data = userInfo?["data"] as? Data,
let dictionary = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
return dictionary
}
} catch let error {
Logger.error(message: error.localizedDescription)
}
return nil
}
static private func getMessagePeerID(userInfo: [AnyHashable:Any]?) -> MCPeerID? {
return userInfo?["from"] as? MCPeerID
}
static private func createMessage(data: [String: String], peerID: MCPeerID)-> String? {
do {
if let message = data["message"],
let name = data["name"] {
var result: String?
let jsonObject = [
"message": message,
"sender": ["id": peerID.displayName, "displayName": name],
] as [String : Any]
let jsonData = try JSONSerialization.data(withJSONObject: jsonObject)
if let jsonString = String(data: jsonData, encoding: .utf8) {
result = jsonString
}
return result
}
} catch let error {
Logger.error(message: error.localizedDescription)
}
return nil
}
}
@@ -0,0 +1,32 @@
import Flutter
class NearbyPeersStreamHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private var timer : Timer?
func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
startSendingUpdates()
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
stopSendingUpdates()
eventSink = nil
return nil
}
func startSendingUpdates() {
self.timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
let devicesList = NearbyDevicesStore.instance.getDevicesToJsonString()
self.eventSink?(devicesList)
}
}
func stopSendingUpdates() {
self.timer?.invalidate()
self.timer = nil
}
}
+137
View File
@@ -0,0 +1,137 @@
import Flutter
import MultipeerConnectivity
import UIKit
public class NearbyServicePlugin: NSObject, FlutterPlugin {
let manager: NearbyManager;
let channel: FlutterMethodChannel
init(manager: NearbyManager, channel: FlutterMethodChannel) {
self.manager = manager
self.channel = channel
}
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "nearby_service",
binaryMessenger: registrar.messenger()
)
let nearbyPeersChannel = FlutterEventChannel(
name: "nearby_service_peers",
binaryMessenger: registrar.messenger()
)
let connectedDeviceChannel = FlutterEventChannel(
name: "nearby_service_connected_device",
binaryMessenger: registrar.messenger()
)
let nearbyPeersStreamHandler = NearbyPeersStreamHandler()
nearbyPeersChannel.setStreamHandler(nearbyPeersStreamHandler)
let connectedDeviceStreamHandler = ConnectedDeviceStreamHandler()
connectedDeviceChannel.setStreamHandler(connectedDeviceStreamHandler)
let manager = NearbyManager()
let instance = NearbyServicePlugin(manager: manager, channel: channel)
registrar.addMethodCallDelegate(instance, channel: channel)
NotificationCenter.default.addObserver(
instance,
selector: #selector(messageReceived),
name: NearbySession.messageReceived,
object: nil
)
}
public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
NearbyDevicesStore.instance.clear()
}
@objc func messageReceived(notification: Notification) {
DispatchQueue.main.async {
let result = NearbyMessageConverter.convert(userInfo: notification.userInfo)
self.channel.invokeMethod("invoke_nearby_service_message_received", arguments: result)
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
case "getPlatformModel":
result(UIDevice.current.name)
case "initialize":
if let deviceName: String = getArgument(for: "deviceName", call: call) {
manager.initialize(for: deviceName, result: result)
} else {
manager.initialize(for: nil, result: result)
}
case "getSavedDeviceName":
manager.getSavedDeviceName(result: result)
case "getCurrentDevice":
manager.getCurrentDevice(result: result)
case "openServicesSettings":
manager.openServicesSettings(result: result)
case "startAdvertising":
manager.startAdvertising(result: result)
case "startBrowsing":
manager.startBrowsing(result: result)
case "stopAdvertising":
manager.stopAdvertising(result: result)
case "stopBrowsing":
manager.stopBrowsing(result: result)
case "getPeers":
manager.getPeers(result: result)
case "invite":
if let deviceId: String = getArgument(for: "deviceId", call: call) {
manager.invite(for: deviceId, result: result)
} else {
result(false)
}
case "acceptInvite":
if let deviceId : String = getArgument(for: "deviceId", call: call) {
manager.acceptInvite(for: deviceId, result: result)
} else {
result(false)
}
case "disconnect":
if let deviceId: String = getArgument(for: "deviceId", call: call) {
manager.disconnect(for: deviceId, result: result)
} else {
result(false)
}
case "send":
if let message: String = getArgument(for: "message", call: call) {
if let receiver : Dictionary<String, AnyObject> = getArgument(for: "receiver", call: call),
let receiverId : String = receiver["id"] as? String
{
manager.send(for: message, with: receiverId, result: result)
} else {
result(false)
}
} else {
result(false)
}
default:
result(FlutterMethodNotImplemented)
}
}
private func getArgument<T>(for name: String, call: FlutterMethodCall) -> T? {
guard let data = call.arguments as? Dictionary<String, AnyObject> else {
return nil
}
guard let argument: T = data[name] as? T else {
return nil
}
return argument
}
}
+51
View File
@@ -0,0 +1,51 @@
//
// NearbySession.swift
// nearby_service
//
// Created by Kseniia Nikitina on 16/1/24.
//
import Foundation
import MultipeerConnectivity
class NearbySession: NSObject {
var session: MCSession!
var state: MCSessionState = MCSessionState.notConnected
private init(peerID: MCPeerID) {
self.session = MCSession(peer: peerID)
}
static let messageReceived = Notification.Name("NearbySessionReceivedMessage")
static func create(peerID: MCPeerID) -> NearbySession {
let instance = NearbySession(peerID: peerID)
instance.session.delegate = instance
return instance
}
}
extension NearbySession: MCSessionDelegate {
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
self.state = state
}
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
NotificationCenter.default.post(
name: NearbySession.messageReceived,
object: nil,
userInfo: ["from": peerID, "data": data]
)
}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
}
}
+23
View File
@@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint nearby_service.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'nearby_service'
s.version = '0.0.1'
s.summary = 'A new Flutter plugin project.'
s.description = <<-DESC
A new Flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end
+113
View File
@@ -0,0 +1,113 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:nearby_service/nearby_service.dart';
import 'package:nearby_service/nearby_service_platform_interface.dart';
import 'package:nearby_service/src/utils/logger.dart';
export 'package:nearby_service/src/platforms/android/android.dart';
export 'package:nearby_service/src/platforms/ios/ios.dart';
export 'src/models/models.dart';
export 'src/utils/utils.dart';
export 'src/types/types.dart';
abstract class NearbyService {
static NearbyService getInstance({NearbyServiceLogLevel? logLevel}) {
if (logLevel != null) {
Logger.level = logLevel;
}
if (Platform.isAndroid) {
Logger.debug('Created Nearby Android Service');
return NearbyAndroidService();
} else if (Platform.isIOS) {
Logger.debug('Created Nearby IOS Service');
return NearbyIOSService();
} else {
throw NearbyServiceException.unsupportedPlatform(
caller: 'getInstance()',
);
}
}
late final NearbyIOSService? ios = get(
onIOS: (e) => e,
);
late final NearbyAndroidService? android = get(
onAndroid: (e) => e,
);
ValueListenable<bool> get isCommunicationChannelConnecting;
Future<String?> getPlatformVersion() {
return NearbyServicePlatform.instance.getPlatformVersion();
}
Future<String?> getPlatformModel() {
return NearbyServicePlatform.instance.getPlatformModel();
}
Future<NearbyDevice?> getCurrentDevice() {
return NearbyServicePlatform.instance.getCurrentDevice();
}
Future<void> openServicesSettings() {
return NearbyServicePlatform.instance.openServicesSettings();
}
Future<List<NearbyDevice>> getPeers() {
return NearbyServicePlatform.instance.getPeers();
}
Stream<List<NearbyDevice>> getPeersStream() {
return NearbyServicePlatform.instance.getPeersStream();
}
Stream<NearbyDevice?> getConnectedDeviceStream(NearbyDevice device) {
return NearbyServicePlatform.instance.getConnectedDeviceStream(device);
}
Future<bool> initialize({
NearbyInitializeData data = const NearbyInitializeData(),
});
Future<bool> discover();
Future<bool> stopDiscovery();
Future<bool> connect(NearbyDevice device);
Future<bool> disconnect(NearbyDevice device);
FutureOr<bool> startCommunicationChannel(
NearbyCommunicationChannelData data,
);
FutureOr<bool> endCommunicationChannel();
FutureOr<bool> send(OutgoingNearbyMessage message);
}
extension NearbyServiceGetterExtension on NearbyService {
T? get<T>({
T Function(NearbyAndroidService)? onAndroid,
T Function(NearbyIOSService)? onIOS,
T Function(NearbyService)? onAny,
}) {
assert(
onAndroid != null || onIOS != null || onAny != null,
'You should provide at least one of (onAndroid, onIOS, onAny)',
);
if (this is NearbyAndroidService && onAndroid != null) {
return onAndroid(this as NearbyAndroidService);
}
if (this is NearbyIOSService && onIOS != null) {
return onIOS(this as NearbyIOSService);
}
if (onAny != null) {
return onAny(this);
}
return null;
}
}
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:nearby_service/nearby_service.dart';
import 'nearby_service_platform_interface.dart';
/// An implementation of [NearbyServicePlatform] that uses method channels.
class MethodChannelNearbyService extends NearbyServicePlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('nearby_service');
@override
Future<String?> getPlatformVersion() {
return methodChannel.invokeMethod<String>('getPlatformVersion');
}
@override
Future<String?> getPlatformModel() {
return methodChannel.invokeMethod<String>('getPlatformModel');
}
@override
Future<NearbyDevice?> getCurrentDevice() async {
return NearbyDeviceMapper.instance.mapToDevice(
await methodChannel.invokeMethod('getCurrentDevice'),
);
}
@override
Future<void> openServicesSettings() async {
await methodChannel.invokeMethod<bool>('openServicesSettings');
}
@override
Future<List<NearbyDevice>> getPeers() async {
return NearbyDeviceMapper.instance.mapToDeviceList(
await methodChannel.invokeMethod('fetchPeers'),
);
}
@override
Stream<List<NearbyDevice>> getPeersStream() {
const peersChannel = EventChannel("nearby_service_peers");
return peersChannel.receiveBroadcastStream().map((e) {
return NearbyDeviceMapper.instance.mapToDeviceList(e);
});
}
@override
Stream<NearbyDevice?> getConnectedDeviceStream(NearbyDevice device) {
const connectedDeviceChannel = EventChannel(
"nearby_service_connected_device",
);
return connectedDeviceChannel.receiveBroadcastStream(device.info.id).map(
(e) => NearbyDeviceMapper.instance.mapToDevice(e),
);
}
}

Some files were not shown because too many files have changed in this diff Show More