updated for android 16 fixing some detection issues

This commit is contained in:
Your Name 2026-01-16 17:32:53 +01:00
parent f3e01a116c
commit e7c34d0de5
34 changed files with 1955 additions and 1453 deletions

98
.gitignore vendored
View File

@ -1,49 +1,49 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof

1348
LICENSE

File diff suppressed because it is too large Load Diff

252
README.md
View File

@ -1,126 +1,126 @@
# DJI-FCC-HACK
A simple Android app that forces DJI N1 remotes to FCC
<img src=".github/light.webp" alt="app" width="200"/><img src=".github/dark.webp" alt="app" width="200"/>
> [!WARNING]
> This only works for drones with DJI N1 remotes. If you have a different remote, this app will not work for you.
## How to use
Download the latest release from the [releases page](https://github.com/M4TH1EU/DJI-FCC-HACK/releases) and install it on your Android device.
> [!IMPORTANT]
> You need to repeat the following steps every time you turn on the drone and/or remote.
Then follow these steps:
1. Turn on the drone and remote and wait a few seconds for them to connect.
2. Connect your phone to the **bottom** USB port of the remote.
3. Click on 'Send FCC Patch'.
4. Disconnect your phone from the bottom USB port of the remote and connect it to the **top** USB port.
5. Enjoy your drone with FCC mode.
## Compatibility
This app should work on any Android device running Android 8 and above.
**Tested on the following drones:**
* DJI Mavic Air 2
* DJI Mini 4K
* DJI Mini 2
* DJI Air 2S
* DJI Neo 2
> [!WARNING]
> Many people reported that this hack doesn't work with the Mini 3, if anyone finds a working hack I could take a look at reverse-engineering it.
> [!NOTE]
> Please let me know if you have tested this app on another drone so I can update this README.
## How do I know if it worked?
Open the DJI Fly app and go to the Transmission tab. Look at the horizontal bar around -90 dBm:
* If it lines up with the 1km mark, your drone is in CE mode.
* If it falls below the 1km mark, your drone is in FCC mode.
*Check the images below for reference.*
| FCC | CE |
| ----------------------------- | --------------------------- |
| ![fcc.webp](.github/fcc.webp) | ![ce.webp](.github/ce.webp) |
## FAQ
### Does this work on iOS?
No, this app is only available for Android.
### Does this work on DJI Smart Controller?
No, this app only works with N1 remotes (the ones without a screen).
### Does this work on DJI XYZ drone?
Maybe? Give it a try and let me know so I can update this README.
### How does this work?
This app simply sends a command to the remote to switch to FCC mode over the USB port.
### Can I use this app to switch back to CE mode?
No, this app only switches to FCC mode. To switch back to CE, turn off the drone and remote, then power them back on.
## Goggles Support
>[!WARNING]
> This app is not related to the following FCC file-based hacks for goggles; they are included here for reference only.
Steps to enable higher power output for DJI Goggles:
**DJI Goggles V1/V2**
* Create a text file named `naco_pwr.txt` with content: `pwr_2`
* Copy it to a microSD card
* Power on Goggles and Air Unit, wait for camera image
* Insert SD card into Goggles and restart
**DJI Goggles 2 / Goggles 3**
* Create an empty file named `ham_cfg_support` (no extension)
* Copy it to a microSD card
* Insert SD card into Goggles
* Power on Goggles
## Air Units
>[!WARNING]
> This app is not related to the following FCC file-based hacks for air units; they are included here for reference only.
Steps to enable FCC mode on DJI video transmitters:
**Air Unit V1**
* Create `naco_pwr.txt` with `pwr_2` inside
* Copy to microSD card, insert into Air Unit
* Power on
**Vista**
* Create `naco_pwr.txt` with `pwr_2` inside
* Power on Vista and connect via USB
* Copy file to Vista storage when it appears
* Power cycle the unit
**Air Unit O3**
* Create empty file `ham_cfg_support`
* Connect O3 via USB
* Copy to O3 storage
* Power cycle
## Credits
This app is based on the work of [galbb](https://mavicpilots.com/members/galbb.148459/) on the [MavicPilots forum](https://mavicpilots.com/threads/mavic-air-2-switch-to-fcc-mode-using-an-android-app.115027/).
# DJI-FCC-HACK
A simple Android app that forces DJI N1 remotes to FCC
<img src=".github/light.webp" alt="app" width="200"/><img src=".github/dark.webp" alt="app" width="200"/>
> [!WARNING]
> This only works for drones with DJI N1 remotes. If you have a different remote, this app will not work for you.
## How to use
Download the latest release from the [releases page](https://github.com/M4TH1EU/DJI-FCC-HACK/releases) and install it on your Android device.
> [!IMPORTANT]
> You need to repeat the following steps every time you turn on the drone and/or remote.
Then follow these steps:
1. Turn on the drone and remote and wait a few seconds for them to connect.
2. Connect your phone to the **bottom** USB port of the remote.
3. Click on 'Send FCC Patch'.
4. Disconnect your phone from the bottom USB port of the remote and connect it to the **top** USB port.
5. Enjoy your drone with FCC mode.
## Compatibility
This app should work on any Android device running Android 8 and above.
**Tested on the following drones:**
* DJI Mavic Air 2
* DJI Mini 4K
* DJI Mini 2
* DJI Air 2S
* DJI Neo 2
> [!WARNING]
> Many people reported that this hack doesn't work with the Mini 3, if anyone finds a working hack I could take a look at reverse-engineering it.
> [!NOTE]
> Please let me know if you have tested this app on another drone so I can update this README.
## How do I know if it worked?
Open the DJI Fly app and go to the Transmission tab. Look at the horizontal bar around -90 dBm:
* If it lines up with the 1km mark, your drone is in CE mode.
* If it falls below the 1km mark, your drone is in FCC mode.
*Check the images below for reference.*
| FCC | CE |
| ----------------------------- | --------------------------- |
| ![fcc.webp](.github/fcc.webp) | ![ce.webp](.github/ce.webp) |
## FAQ
### Does this work on iOS?
No, this app is only available for Android.
### Does this work on DJI Smart Controller?
No, this app only works with N1 remotes (the ones without a screen).
### Does this work on DJI XYZ drone?
Maybe? Give it a try and let me know so I can update this README.
### How does this work?
This app simply sends a command to the remote to switch to FCC mode over the USB port.
### Can I use this app to switch back to CE mode?
No, this app only switches to FCC mode. To switch back to CE, turn off the drone and remote, then power them back on.
## Goggles Support
>[!WARNING]
> This app is not related to the following FCC file-based hacks for goggles; they are included here for reference only.
Steps to enable higher power output for DJI Goggles:
**DJI Goggles V1/V2**
* Create a text file named `naco_pwr.txt` with content: `pwr_2`
* Copy it to a microSD card
* Power on Goggles and Air Unit, wait for camera image
* Insert SD card into Goggles and restart
**DJI Goggles 2 / Goggles 3**
* Create an empty file named `ham_cfg_support` (no extension)
* Copy it to a microSD card
* Insert SD card into Goggles
* Power on Goggles
## Air Units
>[!WARNING]
> This app is not related to the following FCC file-based hacks for air units; they are included here for reference only.
Steps to enable FCC mode on DJI video transmitters:
**Air Unit V1**
* Create `naco_pwr.txt` with `pwr_2` inside
* Copy to microSD card, insert into Air Unit
* Power on
**Vista**
* Create `naco_pwr.txt` with `pwr_2` inside
* Power on Vista and connect via USB
* Copy file to Vista storage when it appears
* Power cycle the unit
**Air Unit O3**
* Create empty file `ham_cfg_support`
* Connect O3 via USB
* Copy to O3 storage
* Power cycle
## Credits
This app is based on the work of [galbb](https://mavicpilots.com/members/galbb.148459/) on the [MavicPilots forum](https://mavicpilots.com/threads/mavic-air-2-switch-to-fcc-mode-using-an-android-app.115027/).

View File

@ -1,60 +1,60 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "ch.mathieubroillet.djiffchack"
compileSdk = 35
defaultConfig {
applicationId = "ch.mathieubroillet.djiffchack"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation("com.github.mik3y:usb-serial-for-android:3.8.1")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "ch.mathieubroillet.djiffchack"
compileSdk = 35
defaultConfig {
applicationId = "ch.mathieubroillet.djiffchack"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation("com.github.mik3y:usb-serial-for-android:3.8.1")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

View File

@ -1,21 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -1,24 +1,24 @@
package ch.mathieubroillet.djiffchack
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("ch.mathieubroillet.djiffchack", appContext.packageName)
}
package ch.mathieubroillet.djiffchack
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("ch.mathieubroillet.djiffchack", appContext.packageName)
}
}

View File

@ -1,38 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
<uses-feature android:name="android.hardware.usb.accessory" android:required="true"/>
<uses-permission android:name="android.permission.USB_PERMISSION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/icon"
android:label="@string/app_name"
android:roundIcon="@mipmap/icon_round"
android:supportsRtl="true"
android:theme="@style/Theme.Djiffchack"
tools:targetApi="31">
<uses-library android:name="com.android.future.usb.accessory"/>
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Djiffchack">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
</intent-filter>
<meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter"/>
</activity>
</application>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
<uses-feature android:name="android.hardware.usb.accessory" android:required="true"/>
<uses-permission android:name="android.permission.USB_PERMISSION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/icon"
android:label="@string/app_name"
android:roundIcon="@mipmap/icon_round"
android:supportsRtl="true"
android:theme="@style/Theme.Djiffchack"
tools:targetApi="31">
<uses-library android:name="com.android.future.usb.accessory"/>
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Djiffchack">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,14 +1,14 @@
package ch.mathieubroillet.djiffchack
object Constants {
private const val PACKAGE = "ch.mathieubroillet.djiffchack"
const val INTENT_ACTION_GRANT_USB_PERMISSION = "$PACKAGE.USB_PERMISSION"
// The "magic bytes" that enable FCC mode
// The credits goes to @galbb from https://mavicpilots.com/threads/mavic-air-2-switch-to-fcc-mode-using-an-android-app.115027/
val BYTES_1 = byteArrayOf(85, 13, 4, 33, 42, 31, 0, 0, 0, 0, 1, -122, 32)
val BYTES_2 = byteArrayOf(85, 24, 4, 32, 2, 9, 0, 0, 64, 9, 39, 0, 2, 72, 0, -1, -1, 2, 0, 0, 0, 0, -127, 31)
const val GITHUB_URL = "https://github.com/M4TH1EU/DJI-FCC-HACK"
package ch.mathieubroillet.djiffchack
object Constants {
private const val PACKAGE = "ch.mathieubroillet.djiffchack"
const val INTENT_ACTION_GRANT_USB_PERMISSION = "$PACKAGE.USB_PERMISSION"
// The "magic bytes" that enable FCC mode
// The credits goes to @galbb from https://mavicpilots.com/threads/mavic-air-2-switch-to-fcc-mode-using-an-android-app.115027/
val BYTES_1 = byteArrayOf(85, 13, 4, 33, 42, 31, 0, 0, 0, 0, 1, -122, 32)
val BYTES_2 = byteArrayOf(85, 24, 4, 32, 2, 9, 0, 0, 64, 9, 39, 0, 2, 72, 0, -1, -1, 2, 0, 0, 0, 0, -127, 31)
const val GITHUB_URL = "https://github.com/M4TH1EU/DJI-FCC-HACK"
}

View File

@ -19,6 +19,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -32,7 +34,9 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@ -45,9 +49,11 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
@ -66,17 +72,44 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
data class UsbDeviceInfo(
val device: UsbDevice,
val isDjiDevice: Boolean,
val hasPermission: Boolean
)
class MainActivity : ComponentActivity() {
private lateinit var usbManager: UsbManager
private var usbConnected by mutableStateOf(false)
private var isPatching by mutableStateOf(false)
private val debugLogs = mutableStateListOf<String>()
private val maxLogs = 50
private val usbDevices = mutableStateListOf<UsbDeviceInfo>()
private var selectedDevice by mutableStateOf<UsbDevice?>(null)
private fun addDebugLog(message: String) {
val timestamp = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()).format(Date())
val logEntry = "[$timestamp] $message"
debugLogs.add(0, logEntry) // Add to the beginning
Log.d("DEBUG_LOG", message)
// Keep only the most recent logs
if (debugLogs.size > maxLogs) {
debugLogs.removeAt(debugLogs.lastIndex)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
addDebugLog("App started")
addDebugLog("Android ${android.os.Build.VERSION.RELEASE} (API ${android.os.Build.VERSION.SDK_INT})")
// Register receiver to detect USB plug/unplug events
val filter = IntentFilter().apply {
@ -85,15 +118,24 @@ class MainActivity : ComponentActivity() {
addAction(Constants.INTENT_ACTION_GRANT_USB_PERMISSION)
}
ContextCompat.registerReceiver(
this, usbReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED
this, usbReceiver, filter, ContextCompat.RECEIVER_EXPORTED
)
// Initial check for USB connection
refreshUsbConnection()
// Initial USB scan
refreshUsbDeviceList()
setContent {
DJI_FCC_HACK_Theme {
MainScreen(usbConnected, ::refreshUsbConnection, ::sendPatch, isPatching)
MainScreen(
usbConnected = usbConnected,
onRefresh = ::refreshUsbDeviceList,
onSendPatch = ::sendPatch,
isPatching = isPatching,
debugLogs = debugLogs,
usbDevices = usbDevices,
selectedDevice = selectedDevice,
onDeviceSelected = ::selectDevice
)
}
}
}
@ -103,29 +145,90 @@ class MainActivity : ComponentActivity() {
unregisterReceiver(usbReceiver)
}
private fun selectDevice(device: UsbDevice) {
addDebugLog("User selected device: VID=${device.vendorId}, PID=${device.productId}")
selectedDevice = device
// Check permission status
val hasPermission = usbManager.hasPermission(device)
addDebugLog("Permission status: $hasPermission")
if (!hasPermission) {
addDebugLog("Requesting permission for selected device...")
usbConnected = false
requestUsbPermission(device)
} else {
addDebugLog("✓ Permission already granted for selected device")
usbConnected = true
}
}
/**
* Refreshes the USB connection status
* Refreshes the USB device list
*/
private fun refreshUsbConnection() {
if (usbManager.deviceList.isNotEmpty()) {
val device: UsbDevice = usbManager.deviceList.values.first()
Log.d("USB_CONNECTION", device.vendorId.toString() + ":" + device.productId.toString())
private fun refreshUsbDeviceList() {
addDebugLog("=== Scanning for USB devices ===")
addDebugLog("Android ${android.os.Build.VERSION.RELEASE} (API ${android.os.Build.VERSION.SDK_INT})")
// Check to be sure the device is the initialized DJI Remote (and not another USB device)
if (device.productId != 4128) {
Log.d("USB_CONNECTION", "Device not supported ${device.productId}")
usbConnected = false
return
}
// Check if USB host is supported
val hasUsbHostFeature = packageManager.hasSystemFeature("android.hardware.usb.host")
addDebugLog("USB Host feature available: $hasUsbHostFeature")
if (usbManager.openDevice(device) == null) {
Log.d("USB_CONNECTION", "Requesting USB Permission")
requestUsbPermission(device)
} else {
usbConnected = true
// Get device list
val deviceList = usbManager.deviceList
addDebugLog("UsbManager.deviceList size: ${deviceList.size}")
if (deviceList.isEmpty()) {
addDebugLog("WARNING: No USB devices found")
addDebugLog("Possible reasons:")
addDebugLog(" - No USB device connected")
addDebugLog(" - Android 16 USB enumeration bug")
addDebugLog(" - USB Protection enabled (unlock phone)")
addDebugLog(" - Try unplugging and re-plugging USB")
// Don't clear usbDevices here - keep showing last known devices
// Only clear connection status
if (selectedDevice != null) {
val stillExists = deviceList.values.any { it == selectedDevice }
if (!stillExists) {
addDebugLog("Selected device no longer in list")
usbConnected = false
selectedDevice = null
}
}
} else {
usbConnected = false
addDebugLog("Found ${deviceList.size} USB device(s):")
// Build new list without clearing immediately
val newDevices = mutableListOf<UsbDeviceInfo>()
deviceList.values.forEachIndexed { index, device ->
val isDji = device.vendorId == 11427 && device.productId == 4128
val hasPerm = usbManager.hasPermission(device)
addDebugLog(" [$index] VID=0x${device.vendorId.toString(16)} PID=0x${device.productId.toString(16)}")
addDebugLog(" Name: ${device.deviceName}")
addDebugLog(" DJI Device: $isDji")
addDebugLog(" Has Permission: $hasPerm")
newDevices.add(UsbDeviceInfo(device, isDji, hasPerm))
// Update selected device permission status if it's in the new list
if (device == selectedDevice) {
usbConnected = hasPerm
addDebugLog("Selected device found, permission: $hasPerm")
}
// Auto-select DJI device if found and has permission and nothing selected
if (isDji && hasPerm && selectedDevice == null) {
selectedDevice = device
usbConnected = true
addDebugLog("✓ Auto-selected DJI device with permission")
}
}
// Update the list only after building complete list
usbDevices.clear()
usbDevices.addAll(newDevices)
}
}
@ -133,12 +236,23 @@ class MainActivity : ComponentActivity() {
* Sends the FCC patch to the DJI remote via USB
*/
private fun sendPatch(): Boolean {
// At this point, we assume the USB device is connected and we have permission to access it
if (!usbConnected) {
Toast.makeText(this, "No USB device connected!", Toast.LENGTH_SHORT).show()
addDebugLog("=== Starting FCC Patch ===")
if (selectedDevice == null) {
addDebugLog("ERROR: No device selected")
Toast.makeText(this, "Please select a USB device first!", Toast.LENGTH_SHORT).show()
return false
}
if (!usbManager.hasPermission(selectedDevice!!)) {
addDebugLog("ERROR: No permission for selected device")
Toast.makeText(this, "No USB permission!", Toast.LENGTH_SHORT).show()
return false
}
val device = selectedDevice!!
addDebugLog("Using device: VID=${device.vendorId}, PID=${device.productId}")
val probeTable = ProbeTable().apply {
addProduct(11427, 4128, CdcAcmSerialDriver::class.java)
@ -148,30 +262,41 @@ class MainActivity : ComponentActivity() {
}
// Retrieve the custom device (DJI remote) with the correct driver from the probe table above
val driver = UsbSerialProber(probeTable).probeDevice(usbManager.deviceList.values.first())
addDebugLog("Probing USB device...")
val driver = UsbSerialProber(probeTable).probeDevice(device)
addDebugLog("Opening USB device...")
val deviceConnection = usbManager.openDevice(driver.device)
if (deviceConnection == null) {
Log.e("USB_PATCH", "Error opening USB device")
addDebugLog("ERROR: Failed to open USB device")
Toast.makeText(this, "Error opening USB device", Toast.LENGTH_SHORT).show()
return false
}
try {
addDebugLog("Getting serial port...")
val deviceSerialPort = driver.ports.firstOrNull()
if (deviceSerialPort == null) {
Log.e("USB_PATCH", "Error opening USB port")
addDebugLog("ERROR: No serial port found")
Toast.makeText(this, "Error opening USB port", Toast.LENGTH_SHORT).show()
return false
}
addDebugLog("Opening serial port...")
deviceSerialPort.open(deviceConnection)
addDebugLog("Setting parameters (19200 baud, 8N1)...")
deviceSerialPort.setParameters(19200, 8, 1, UsbSerialPort.PARITY_NONE)
addDebugLog("Writing patch bytes (part 1)...")
deviceSerialPort.write(Constants.BYTES_1, 1000)
addDebugLog("Writing patch bytes (part 2)...")
deviceSerialPort.write(Constants.BYTES_2, 1000)
addDebugLog("SUCCESS: Patch sent successfully!")
Toast.makeText(this, "Patched successfully", Toast.LENGTH_LONG).show()
} catch (e: Exception) {
e.printStackTrace()
addDebugLog("ERROR: ${e.javaClass.simpleName}: ${e.message}")
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
}
@ -183,14 +308,27 @@ class MainActivity : ComponentActivity() {
* Requests USB permission for the device
*/
private fun requestUsbPermission(device: UsbDevice) {
addDebugLog("Creating permission request intent...")
addDebugLog("Package name: $packageName")
val permissionIntent = PendingIntent.getBroadcast(
this,
0,
Intent(Constants.INTENT_ACTION_GRANT_USB_PERMISSION).apply { setPackage(packageName) },
PendingIntent.FLAG_MUTABLE
Intent(Constants.INTENT_ACTION_GRANT_USB_PERMISSION).apply {
setPackage(packageName)
putExtra(UsbManager.EXTRA_DEVICE, device)
},
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
usbManager.requestPermission(device, permissionIntent)
addDebugLog("Calling usbManager.requestPermission()...")
try {
usbManager.requestPermission(device, permissionIntent)
addDebugLog("Permission request sent successfully")
} catch (e: Exception) {
addDebugLog("ERROR requesting permission: ${e.message}")
e.printStackTrace()
}
}
@ -202,18 +340,51 @@ class MainActivity : ComponentActivity() {
when (intent.action) {
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
Log.d("USB_EVENT", "USB Device Connected")
refreshUsbConnection()
addDebugLog("EVENT: USB device attached")
refreshUsbDeviceList()
}
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
Log.d("USB_EVENT", "USB Device Disconnected")
refreshUsbConnection()
addDebugLog("EVENT: USB device detached")
val device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
if (device == selectedDevice) {
addDebugLog("Selected device was detached")
selectedDevice = null
usbConnected = false
}
refreshUsbDeviceList()
}
Constants.INTENT_ACTION_GRANT_USB_PERMISSION -> {
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
val device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice::class.java)
val granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
addDebugLog("Permission response for device: VID=${device?.vendorId}, PID=${device?.productId}")
addDebugLog("Permission granted: $granted")
if (granted) {
Log.d("USB_EVENT", "USB Permission Granted")
refreshUsbConnection()
addDebugLog("✓ USB permission granted by user")
// Verify permission with hasPermission
if (device != null) {
val verified = usbManager.hasPermission(device)
addDebugLog("Permission verified with hasPermission(): $verified")
if (device == selectedDevice && verified) {
usbConnected = true
addDebugLog("✓ Selected device now connected")
}
}
// Refresh immediately
refreshUsbDeviceList()
} else {
addDebugLog("❌ USB permission denied by user")
if (device == selectedDevice) {
usbConnected = false
}
}
}
}
@ -227,14 +398,25 @@ fun MainScreen(
usbConnected: Boolean,
onRefresh: () -> Unit,
onSendPatch: () -> Boolean,
isPatching: Boolean = false
isPatching: Boolean = false,
debugLogs: SnapshotStateList<String> = mutableStateListOf(),
usbDevices: SnapshotStateList<UsbDeviceInfo> = mutableStateListOf(),
selectedDevice: UsbDevice? = null,
onDeviceSelected: (UsbDevice) -> Unit = {}
) {
var buttonText by remember { mutableStateOf("Send FCC Patch") }
var buttonEnabled by remember { mutableStateOf(true) }
var showDebugLog by remember { mutableStateOf(false) }
val uriHandler = LocalUriHandler.current
Scaffold(topBar = {
TopAppBar(title = { Text("DJI FCC Hack") }, actions = {
androidx.compose.material3.TextButton(onClick = { showDebugLog = !showDebugLog }) {
Text(
text = if (showDebugLog) "Hide Log" else "Show Log",
style = MaterialTheme.typography.labelMedium
)
}
IconButton(onClick = onRefresh, enabled = !isPatching) {
Icon(Icons.Default.Refresh, contentDescription = "Refresh USB Connection")
}
@ -256,25 +438,25 @@ fun MainScreen(
modifier = Modifier.size(75.dp),
)
// Disclaimer Section
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Disclaimer",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "This app is provided as-is and is not affiliated with DJI. Use at your own risk.",
style = MaterialTheme.typography.bodyMedium
)
}
}
// // Disclaimer Section
// Card(
// modifier = Modifier.fillMaxWidth(),
// shape = RoundedCornerShape(16.dp),
// colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer)
// ) {
// Column(modifier = Modifier.padding(16.dp)) {
// Text(
// text = "Disclaimer",
// style = MaterialTheme.typography.titleMedium,
// fontWeight = FontWeight.Bold
// )
// Spacer(modifier = Modifier.height(4.dp))
// Text(
// text = "This app is provided as-is and is not affiliated with DJI. Use at your own risk.",
// style = MaterialTheme.typography.bodyMedium
// )
// }
// }
// Instructions Section
Card(
@ -311,28 +493,240 @@ fun MainScreen(
}
}
// USB Connection Status
// USB Device Selection Card
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = if (usbConnected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.errorContainer
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
Column(modifier = Modifier.padding(16.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
Icons.Default.Build,
contentDescription = "USB Devices",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "USB Devices (${usbDevices.size})",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(8.dp))
if (usbDevices.isEmpty()) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = "No USB devices found",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.error
)
// Text(
// text = "Troubleshooting steps:",
// style = MaterialTheme.typography.bodySmall,
// fontWeight = FontWeight.Bold,
// color = MaterialTheme.colorScheme.onSurfaceVariant
// )
// Column(modifier = Modifier.padding(start = 8.dp)) {
// listOf(
// "Make sure phone is UNLOCKED",
// "Try unplugging and re-plugging USB",
// "Check USB notification - change to 'File Transfer' mode",
// "Enable USB debugging in Developer options",
// "Known bug in Android 16 - may not work reliably"
// ).forEach { tip ->
// Row(modifier = Modifier.padding(vertical = 2.dp)) {
// Text("• ", style = MaterialTheme.typography.bodySmall)
// Text(
// text = tip,
// style = MaterialTheme.typography.bodySmall,
// color = MaterialTheme.colorScheme.onSurfaceVariant
// )
// }
// }
// }
// Spacer(modifier = Modifier.height(4.dp))
// Text(
// text = "Tap refresh (↻) after trying each step",
// style = MaterialTheme.typography.bodySmall,
// fontStyle = FontStyle.Italic,
// color = MaterialTheme.colorScheme.primary
// )
}
} else {
usbDevices.forEach { deviceInfo ->
val isSelected = deviceInfo.device == selectedDevice
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clickable { onDeviceSelected(deviceInfo.device) }
.then(
if (isSelected) {
Modifier.border(
3.dp,
MaterialTheme.colorScheme.primary,
RoundedCornerShape(8.dp)
)
} else {
Modifier
}
),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
MaterialTheme.colorScheme.surfaceContainerHighest
)
) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "VID: 0x${deviceInfo.device.vendorId.toString(16)} / PID: 0x${deviceInfo.device.productId.toString(16)}",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
if (deviceInfo.isDjiDevice) {
Spacer(modifier = Modifier.width(8.dp))
Icon(
Icons.Default.Star,
contentDescription = "DJI Device",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
}
}
Text(
text = deviceInfo.device.deviceName,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
if (deviceInfo.isDjiDevice) {
Text(
text = "✓ DJI Remote Controller",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = if (deviceInfo.hasPermission) "✓ Has Permission" else "⚠ No Permission",
style = MaterialTheme.typography.bodySmall,
color = if (deviceInfo.hasPermission)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.error
)
if (!deviceInfo.hasPermission) {
androidx.compose.material3.TextButton(
onClick = { onDeviceSelected(deviceInfo.device) },
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 0.dp),
modifier = Modifier.height(24.dp)
) {
Text(
text = "Grant",
style = MaterialTheme.typography.labelSmall
)
}
}
}
}
if (isSelected) {
Icon(
Icons.Default.CheckCircle,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
}
}
}
}
}
}
// Debug Log Section
if (showDebugLog) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Icon(
if (usbConnected) Icons.Default.CheckCircle else Icons.Default.Clear,
contentDescription = "USB Status"
)
Spacer(Modifier.width(8.dp))
Text(
text = if (usbConnected) "Remote Connected" else "Remote Not Connected",
style = MaterialTheme.typography.bodyMedium
)
Column(modifier = Modifier.padding(16.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
Icons.Default.Info,
contentDescription = "Debug Info",
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Debug Log",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(8.dp))
if (debugLogs.isEmpty()) {
Text(
text = "No logs yet...",
style = MaterialTheme.typography.bodySmall,
fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
Card(
modifier = Modifier
.fillMaxWidth()
.height(200.dp),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(8.dp)
) {
debugLogs.forEach { log ->
Text(
text = log,
style = MaterialTheme.typography.bodySmall,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
modifier = Modifier.padding(vertical = 2.dp)
)
}
}
}
}
}
}
}
@ -353,13 +747,22 @@ fun MainScreen(
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = usbConnected && !isPatching && buttonEnabled
enabled = selectedDevice != null && !isPatching && buttonEnabled
) {
Icon(Icons.Default.Build, contentDescription = "Patch")
Spacer(Modifier.width(8.dp))
Text(buttonText)
}
if (selectedDevice == null && usbDevices.isNotEmpty()) {
Text(
text = "Please select a USB device above",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
fontStyle = FontStyle.Italic
)
}
// Links
Row {
IconButton(onClick = { uriHandler.openUri(Constants.GITHUB_URL) }) {
@ -384,6 +787,17 @@ fun MainScreen(
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.secondary,
)
Text(
text = " updated by ",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary
)
Text(
text = "luhf",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.secondary,
)
}
Text(

View File

@ -1,11 +1,11 @@
package ch.mathieubroillet.djiffchack.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
package ch.mathieubroillet.djiffchack.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -1,57 +1,57 @@
package ch.mathieubroillet.djiffchack.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun DJI_FCC_HACK_Theme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
package ch.mathieubroillet.djiffchack.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun DJI_FCC_HACK_Theme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -1,34 +1,34 @@
package ch.mathieubroillet.djiffchack.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
package ch.mathieubroillet.djiffchack.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@ -1,15 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="46.51dp"
android:height="26.87dp"
android:viewportWidth="164.78"
android:viewportHeight="95.2">
<path
android:pathData="m120.64,69.13 l12.16,-50.5l-26.1,-0l-11.12,45.29c-1.61,8.83 -11.1,12.96 -17.84,13.06l-18.49,-0l-6.27,18.2l38.86,-0c9.59,-0 23.74,-4.91 28.81,-26.06"
android:fillColor="#000000"/>
<path
android:pathData="m65.86,51.32 l12.27,-51.33l26.86,-0l-13.96,58.39c-2.69,11.27 -11.07,13.98 -18.81,13.98l-61.91,-0c-6.82,-0 -12.54,-2.9 -9.44,-15.92l5.57,-23.29c2.83,-11.81 11.61,-14.51 17.96,-14.51l43.21,-0l-3.48,14.55l-22.06,-0c-3.24,-0 -5.02,0.7 -5.93,4.49l-3.56,14.87c-1.27,5.34 0.59,5.71 4.5,5.71l20.21,-0c3.7,-0 6.95,-0.23 8.56,-6.94"
android:fillColor="#000000"/>
<path
android:pathData="m138.69,18.63 l-12.64,53.73l26.09,-0l12.64,-53.73l-26.09,-0z"
android:fillColor="#000000"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="46.51dp"
android:height="26.87dp"
android:viewportWidth="164.78"
android:viewportHeight="95.2">
<path
android:pathData="m120.64,69.13 l12.16,-50.5l-26.1,-0l-11.12,45.29c-1.61,8.83 -11.1,12.96 -17.84,13.06l-18.49,-0l-6.27,18.2l38.86,-0c9.59,-0 23.74,-4.91 28.81,-26.06"
android:fillColor="#000000"/>
<path
android:pathData="m65.86,51.32 l12.27,-51.33l26.86,-0l-13.96,58.39c-2.69,11.27 -11.07,13.98 -18.81,13.98l-61.91,-0c-6.82,-0 -12.54,-2.9 -9.44,-15.92l5.57,-23.29c2.83,-11.81 11.61,-14.51 17.96,-14.51l43.21,-0l-3.48,14.55l-22.06,-0c-3.24,-0 -5.02,0.7 -5.93,4.49l-3.56,14.87c-1.27,5.34 0.59,5.71 4.5,5.71l20.21,-0c3.7,-0 6.95,-0.23 8.56,-6.94"
android:fillColor="#000000"/>
<path
android:pathData="m138.69,18.63 l-12.64,53.73l26.09,-0l12.64,-53.73l-26.09,-0z"
android:fillColor="#000000"/>
</vector>

View File

@ -1,15 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="46.51dp"
android:height="26.87dp"
android:viewportWidth="164.78"
android:viewportHeight="95.2">
<path
android:pathData="m120.64,69.13 l12.16,-50.5l-26.1,-0l-11.12,45.29c-1.61,8.83 -11.1,12.96 -17.84,13.06l-18.49,-0l-6.27,18.2l38.86,-0c9.59,-0 23.74,-4.91 28.81,-26.06"
android:fillColor="#fff"/>
<path
android:pathData="m65.86,51.32 l12.27,-51.33l26.86,-0l-13.96,58.39c-2.69,11.27 -11.07,13.98 -18.81,13.98l-61.91,-0c-6.82,-0 -12.54,-2.9 -9.44,-15.92l5.57,-23.29c2.83,-11.81 11.61,-14.51 17.96,-14.51l43.21,-0l-3.48,14.55l-22.06,-0c-3.24,-0 -5.02,0.7 -5.93,4.49l-3.56,14.87c-1.27,5.34 0.59,5.71 4.5,5.71l20.21,-0c3.7,-0 6.95,-0.23 8.56,-6.94"
android:fillColor="#fff"/>
<path
android:pathData="m138.69,18.63 l-12.64,53.73l26.09,-0l12.64,-53.73l-26.09,-0z"
android:fillColor="#fff"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="46.51dp"
android:height="26.87dp"
android:viewportWidth="164.78"
android:viewportHeight="95.2">
<path
android:pathData="m120.64,69.13 l12.16,-50.5l-26.1,-0l-11.12,45.29c-1.61,8.83 -11.1,12.96 -17.84,13.06l-18.49,-0l-6.27,18.2l38.86,-0c9.59,-0 23.74,-4.91 28.81,-26.06"
android:fillColor="#fff"/>
<path
android:pathData="m65.86,51.32 l12.27,-51.33l26.86,-0l-13.96,58.39c-2.69,11.27 -11.07,13.98 -18.81,13.98l-61.91,-0c-6.82,-0 -12.54,-2.9 -9.44,-15.92l5.57,-23.29c2.83,-11.81 11.61,-14.51 17.96,-14.51l43.21,-0l-3.48,14.55l-22.06,-0c-3.24,-0 -5.02,0.7 -5.93,4.49l-3.56,14.87c-1.27,5.34 0.59,5.71 4.5,5.71l20.21,-0c3.7,-0 6.95,-0.23 8.56,-6.94"
android:fillColor="#fff"/>
<path
android:pathData="m138.69,18.63 l-12.64,53.73l26.09,-0l12.64,-53.73l-26.09,-0z"
android:fillColor="#fff"/>
</vector>

View File

@ -1,35 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1066"
android:viewportHeight="1053">
<path
android:pathData="m211.8,0.9h642.4c116.7,0 211.2,94.6 211.2,211.2v629c0,116.6 -94.5,211.1 -211.2,211.1h-642.4c-116.6,0 -211.2,-94.5 -211.2,-211.1v-629c0,-116.6 94.6,-211.2 211.2,-211.2z"
android:fillColor="#be3876"/>
<path
android:pathData="m1.9,838.7l-0.1,-395.1 511.6,608.7h-297.9c-123.3,-1.5 -213.2,-92.4 -213.6,-213.6z"
android:fillColor="#9b2b5f"/>
<path
android:pathData="m278.6,772.9l-76.1,-90.6 -85,347.3c41.6,22.7 82.8,22 128,22.7z"
android:fillColor="#a62862"/>
<path
android:pathData="m245.5,1052.3h267.9l-234.8,-279.4z"
android:fillColor="#a0406c"/>
<path
android:pathData="m278.6,772.9l80.9,-772 -157,681.4z"
android:fillColor="#c44b83"/>
<path
android:pathData="m278.6,772.9l80.9,-772h172.9l314.3,374.9 -160.7,676.4 -172.6,0.1z"
android:fillColor="#a62862"/>
<path
android:pathData="m1065.4,510.1l-533.4,-509.2 533.4,635.5z"
android:fillColor="#c44b83"/>
<path
android:pathData="m1065.4,510.1l-533.4,-509.2h322.2c122.3,0.1 211.4,101.1 211.2,211.2z"
android:fillColor="#a0406c"/>
<path
android:pathData="m686,1052.5c133,-56.7 272.3,-129.9 379.2,-252.8v54.5c0,85.5 -85.9,198.3 -205.5,198.3z"
android:strokeAlpha="0.1"
android:fillColor="#ac3a6f"
android:fillAlpha="0.1"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1066"
android:viewportHeight="1053">
<path
android:pathData="m211.8,0.9h642.4c116.7,0 211.2,94.6 211.2,211.2v629c0,116.6 -94.5,211.1 -211.2,211.1h-642.4c-116.6,0 -211.2,-94.5 -211.2,-211.1v-629c0,-116.6 94.6,-211.2 211.2,-211.2z"
android:fillColor="#be3876"/>
<path
android:pathData="m1.9,838.7l-0.1,-395.1 511.6,608.7h-297.9c-123.3,-1.5 -213.2,-92.4 -213.6,-213.6z"
android:fillColor="#9b2b5f"/>
<path
android:pathData="m278.6,772.9l-76.1,-90.6 -85,347.3c41.6,22.7 82.8,22 128,22.7z"
android:fillColor="#a62862"/>
<path
android:pathData="m245.5,1052.3h267.9l-234.8,-279.4z"
android:fillColor="#a0406c"/>
<path
android:pathData="m278.6,772.9l80.9,-772 -157,681.4z"
android:fillColor="#c44b83"/>
<path
android:pathData="m278.6,772.9l80.9,-772h172.9l314.3,374.9 -160.7,676.4 -172.6,0.1z"
android:fillColor="#a62862"/>
<path
android:pathData="m1065.4,510.1l-533.4,-509.2 533.4,635.5z"
android:fillColor="#c44b83"/>
<path
android:pathData="m1065.4,510.1l-533.4,-509.2h322.2c122.3,0.1 211.4,101.1 211.2,211.2z"
android:fillColor="#a0406c"/>
<path
android:pathData="m686,1052.5c133,-56.7 272.3,-129.9 379.2,-252.8v54.5c0,85.5 -85.9,198.3 -205.5,198.3z"
android:strokeAlpha="0.1"
android:fillColor="#ac3a6f"
android:fillAlpha="0.1"/>
</vector>

View File

@ -1,14 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="633"
android:viewportHeight="586">
<group android:scaleX="0.67"
android:scaleY="0.67"
android:translateX="104.445"
android:translateY="96.69">
<path
android:pathData="m319.7,122.6v100.2q0,5.7 -1.5,10.4 -1.4,4.5 -3.8,8 -2.4,3.4 -5.6,5.9 -3.1,2.4 -6.7,3.9 -3.4,1.5 -7,2.3 -3.5,0.7 -6.6,0.7h-40.1q-5.7,0 -10.3,-1.5 -4.5,-1.4 -8,-3.8 -3.4,-2.5 -5.9,-5.6 -2.4,-3.2 -3.9,-6.6 -1.6,-3.5 -2.3,-7 -0.7,-3.6 -0.7,-6.7v-30.1q0,-8.5 3.1,-14.4 3.1,-6 7.7,-9.7 4.7,-3.7 10.2,-5.3 5.5,-1.7 10.1,-1.7h40.1v22.2h-39.9q-4.5,0 -6.8,2.4 -2.3,2.2 -2.3,6.5v29.9q0,4.6 2.2,6.9 2.3,2.3 6.7,2.3h40.1q4.5,0 6.7,-2.3 2.2,-2.3 2.2,-6.7v-100.2zM363.5,122.6v20.1h-22.2v-20.1zM363.5,161.6v101.3q0,5.7 -1.5,10.3 -1.4,4.6 -3.8,8.1 -2.4,3.4 -5.6,5.9 -3.1,2.3 -6.6,3.9 -3.5,1.5 -7,2.3 -3.5,0.7 -6.6,0.7h-40.1v-22.2h40.1q4.4,0 6.6,-2.3 2.3,-2.4 2.3,-6.7v-101.3zM407.3,122.6v20.1h-22.2v-20.1zM407.3,161.6v92.4h-22.2v-92.4zM232.4,391.5v22.4h-70.2v-22.4zM242.3,341.5v22.2h-89.1v100.3h-22.2v-111.3q0,-2.3 0.9,-4.4 0.8,-2 2.3,-3.6 1.5,-1.5 3.6,-2.4 2,-0.8 4.4,-0.8zM370,441.8v22.2h-80.2q-3.1,0 -6.6,-0.7 -3.6,-0.8 -7.1,-2.3 -3.4,-1.5 -6.5,-3.9 -3.2,-2.5 -5.7,-5.9 -2.4,-3.5 -3.8,-8 -1.5,-4.7 -1.5,-10.4v-60.1q0,-3.1 0.7,-6.6 0.8,-3.6 2.3,-7 1.6,-3.5 4,-6.7 2.5,-3.2 5.9,-5.6 3.5,-2.4 8.1,-3.9 4.5,-1.4 10.2,-1.4h80.2v22.2h-80.2q-4.3,0 -6.6,2.3 -2.4,2.3 -2.4,6.8v60q0,4.3 2.4,6.7 2.3,2.3 6.6,2.3zM499.8,441.8v22.2h-80.1q-3.1,0 -6.7,-0.7 -3.5,-0.8 -7,-2.3 -3.4,-1.5 -6.6,-3.9 -3.2,-2.5 -5.6,-5.9 -2.4,-3.5 -3.9,-8 -1.4,-4.7 -1.4,-10.4v-60.1q0,-3.1 0.7,-6.6 0.7,-3.6 2.3,-7 1.5,-3.5 4,-6.7 2.5,-3.2 5.9,-5.6 3.5,-2.4 8,-3.9 4.5,-1.4 10.3,-1.4h80.1v22.2h-80.1q-4.4,0 -6.7,2.3 -2.3,2.3 -2.3,6.8v60q0,4.3 2.3,6.7 2.4,2.3 6.7,2.3z"
android:fillColor="#fff"/>
</group>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="633"
android:viewportHeight="586">
<group android:scaleX="0.67"
android:scaleY="0.67"
android:translateX="104.445"
android:translateY="96.69">
<path
android:pathData="m319.7,122.6v100.2q0,5.7 -1.5,10.4 -1.4,4.5 -3.8,8 -2.4,3.4 -5.6,5.9 -3.1,2.4 -6.7,3.9 -3.4,1.5 -7,2.3 -3.5,0.7 -6.6,0.7h-40.1q-5.7,0 -10.3,-1.5 -4.5,-1.4 -8,-3.8 -3.4,-2.5 -5.9,-5.6 -2.4,-3.2 -3.9,-6.6 -1.6,-3.5 -2.3,-7 -0.7,-3.6 -0.7,-6.7v-30.1q0,-8.5 3.1,-14.4 3.1,-6 7.7,-9.7 4.7,-3.7 10.2,-5.3 5.5,-1.7 10.1,-1.7h40.1v22.2h-39.9q-4.5,0 -6.8,2.4 -2.3,2.2 -2.3,6.5v29.9q0,4.6 2.2,6.9 2.3,2.3 6.7,2.3h40.1q4.5,0 6.7,-2.3 2.2,-2.3 2.2,-6.7v-100.2zM363.5,122.6v20.1h-22.2v-20.1zM363.5,161.6v101.3q0,5.7 -1.5,10.3 -1.4,4.6 -3.8,8.1 -2.4,3.4 -5.6,5.9 -3.1,2.3 -6.6,3.9 -3.5,1.5 -7,2.3 -3.5,0.7 -6.6,0.7h-40.1v-22.2h40.1q4.4,0 6.6,-2.3 2.3,-2.4 2.3,-6.7v-101.3zM407.3,122.6v20.1h-22.2v-20.1zM407.3,161.6v92.4h-22.2v-92.4zM232.4,391.5v22.4h-70.2v-22.4zM242.3,341.5v22.2h-89.1v100.3h-22.2v-111.3q0,-2.3 0.9,-4.4 0.8,-2 2.3,-3.6 1.5,-1.5 3.6,-2.4 2,-0.8 4.4,-0.8zM370,441.8v22.2h-80.2q-3.1,0 -6.6,-0.7 -3.6,-0.8 -7.1,-2.3 -3.4,-1.5 -6.5,-3.9 -3.2,-2.5 -5.7,-5.9 -2.4,-3.5 -3.8,-8 -1.5,-4.7 -1.5,-10.4v-60.1q0,-3.1 0.7,-6.6 0.8,-3.6 2.3,-7 1.6,-3.5 4,-6.7 2.5,-3.2 5.9,-5.6 3.5,-2.4 8.1,-3.9 4.5,-1.4 10.2,-1.4h80.2v22.2h-80.2q-4.3,0 -6.6,2.3 -2.4,2.3 -2.4,6.8v60q0,4.3 2.4,6.7 2.3,2.3 6.6,2.3zM499.8,441.8v22.2h-80.1q-3.1,0 -6.7,-0.7 -3.5,-0.8 -7,-2.3 -3.4,-1.5 -6.6,-3.9 -3.2,-2.5 -5.6,-5.9 -2.4,-3.5 -3.9,-8 -1.4,-4.7 -1.4,-10.4v-60.1q0,-3.1 0.7,-6.6 0.7,-3.6 2.3,-7 1.5,-3.5 4,-6.7 2.5,-3.2 5.9,-5.6 3.5,-2.4 8,-3.9 4.5,-1.4 10.3,-1.4h80.1v22.2h-80.1q-4.4,0 -6.7,2.3 -2.3,2.3 -2.3,6.8v60q0,4.3 2.3,6.7 2.4,2.3 6.7,2.3z"
android:fillColor="#fff"/>
</group>
</vector>

View File

@ -1,35 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1066"
android:viewportHeight="1053">
<path
android:pathData="m211.8,0.9h642.4c116.7,0 211.2,94.6 211.2,211.2v629c0,116.6 -94.5,211.1 -211.2,211.1h-642.4c-116.6,0 -211.2,-94.5 -211.2,-211.1v-629c0,-116.6 94.6,-211.2 211.2,-211.2z"
android:fillColor="#be3876"/>
<path
android:pathData="m1.9,838.7l-0.1,-395.1 511.6,608.7h-297.9c-123.3,-1.5 -213.2,-92.4 -213.6,-213.6z"
android:fillColor="#9b2b5f"/>
<path
android:pathData="m278.6,772.9l-76.1,-90.6 -85,347.3c41.6,22.7 82.8,22 128,22.7z"
android:fillColor="#a62862"/>
<path
android:pathData="m245.5,1052.3h267.9l-234.8,-279.4z"
android:fillColor="#a0406c"/>
<path
android:pathData="m278.6,772.9l80.9,-772 -157,681.4z"
android:fillColor="#c44b83"/>
<path
android:pathData="m278.6,772.9l80.9,-772h172.9l314.3,374.9 -160.7,676.4 -172.6,0.1z"
android:fillColor="#a62862"/>
<path
android:pathData="m1065.4,510.1l-533.4,-509.2 533.4,635.5z"
android:fillColor="#c44b83"/>
<path
android:pathData="m1065.4,510.1l-533.4,-509.2h322.2c122.3,0.1 211.4,101.1 211.2,211.2z"
android:fillColor="#a0406c"/>
<path
android:pathData="m686,1052.5c133,-56.7 272.3,-129.9 379.2,-252.8v54.5c0,85.5 -85.9,198.3 -205.5,198.3z"
android:strokeAlpha="0.1"
android:fillColor="#ac3a6f"
android:fillAlpha="0.1"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="1066"
android:viewportHeight="1053">
<path
android:pathData="m211.8,0.9h642.4c116.7,0 211.2,94.6 211.2,211.2v629c0,116.6 -94.5,211.1 -211.2,211.1h-642.4c-116.6,0 -211.2,-94.5 -211.2,-211.1v-629c0,-116.6 94.6,-211.2 211.2,-211.2z"
android:fillColor="#be3876"/>
<path
android:pathData="m1.9,838.7l-0.1,-395.1 511.6,608.7h-297.9c-123.3,-1.5 -213.2,-92.4 -213.6,-213.6z"
android:fillColor="#9b2b5f"/>
<path
android:pathData="m278.6,772.9l-76.1,-90.6 -85,347.3c41.6,22.7 82.8,22 128,22.7z"
android:fillColor="#a62862"/>
<path
android:pathData="m245.5,1052.3h267.9l-234.8,-279.4z"
android:fillColor="#a0406c"/>
<path
android:pathData="m278.6,772.9l80.9,-772 -157,681.4z"
android:fillColor="#c44b83"/>
<path
android:pathData="m278.6,772.9l80.9,-772h172.9l314.3,374.9 -160.7,676.4 -172.6,0.1z"
android:fillColor="#a62862"/>
<path
android:pathData="m1065.4,510.1l-533.4,-509.2 533.4,635.5z"
android:fillColor="#c44b83"/>
<path
android:pathData="m1065.4,510.1l-533.4,-509.2h322.2c122.3,0.1 211.4,101.1 211.2,211.2z"
android:fillColor="#a0406c"/>
<path
android:pathData="m686,1052.5c133,-56.7 272.3,-129.9 379.2,-252.8v54.5c0,85.5 -85.9,198.3 -205.5,198.3z"
android:strokeAlpha="0.1"
android:fillColor="#ac3a6f"
android:fillAlpha="0.1"/>
</vector>

View File

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="633"
android:viewportHeight="586">
<path
android:pathData="m319.7,122.6v100.2q0,5.7 -1.5,10.4 -1.4,4.5 -3.8,8 -2.4,3.4 -5.6,5.9 -3.1,2.4 -6.7,3.9 -3.4,1.5 -7,2.3 -3.5,0.7 -6.6,0.7h-40.1q-5.7,0 -10.3,-1.5 -4.5,-1.4 -8,-3.8 -3.4,-2.5 -5.9,-5.6 -2.4,-3.2 -3.9,-6.6 -1.6,-3.5 -2.3,-7 -0.7,-3.6 -0.7,-6.7v-30.1q0,-8.5 3.1,-14.4 3.1,-6 7.7,-9.7 4.7,-3.7 10.2,-5.3 5.5,-1.7 10.1,-1.7h40.1v22.2h-39.9q-4.5,0 -6.8,2.4 -2.3,2.2 -2.3,6.5v29.9q0,4.6 2.2,6.9 2.3,2.3 6.7,2.3h40.1q4.5,0 6.7,-2.3 2.2,-2.3 2.2,-6.7v-100.2zM363.5,122.6v20.1h-22.2v-20.1zM363.5,161.6v101.3q0,5.7 -1.5,10.3 -1.4,4.6 -3.8,8.1 -2.4,3.4 -5.6,5.9 -3.1,2.3 -6.6,3.9 -3.5,1.5 -7,2.3 -3.5,0.7 -6.6,0.7h-40.1v-22.2h40.1q4.4,0 6.6,-2.3 2.3,-2.4 2.3,-6.7v-101.3zM407.3,122.6v20.1h-22.2v-20.1zM407.3,161.6v92.4h-22.2v-92.4zM232.4,391.5v22.4h-70.2v-22.4zM242.3,341.5v22.2h-89.1v100.3h-22.2v-111.3q0,-2.3 0.9,-4.4 0.8,-2 2.3,-3.6 1.5,-1.5 3.6,-2.4 2,-0.8 4.4,-0.8zM370,441.8v22.2h-80.2q-3.1,0 -6.6,-0.7 -3.6,-0.8 -7.1,-2.3 -3.4,-1.5 -6.5,-3.9 -3.2,-2.5 -5.7,-5.9 -2.4,-3.5 -3.8,-8 -1.5,-4.7 -1.5,-10.4v-60.1q0,-3.1 0.7,-6.6 0.8,-3.6 2.3,-7 1.6,-3.5 4,-6.7 2.5,-3.2 5.9,-5.6 3.5,-2.4 8.1,-3.9 4.5,-1.4 10.2,-1.4h80.2v22.2h-80.2q-4.3,0 -6.6,2.3 -2.4,2.3 -2.4,6.8v60q0,4.3 2.4,6.7 2.3,2.3 6.6,2.3zM499.8,441.8v22.2h-80.1q-3.1,0 -6.7,-0.7 -3.5,-0.8 -7,-2.3 -3.4,-1.5 -6.6,-3.9 -3.2,-2.5 -5.6,-5.9 -2.4,-3.5 -3.9,-8 -1.4,-4.7 -1.4,-10.4v-60.1q0,-3.1 0.7,-6.6 0.7,-3.6 2.3,-7 1.5,-3.5 4,-6.7 2.5,-3.2 5.9,-5.6 3.5,-2.4 8,-3.9 4.5,-1.4 10.3,-1.4h80.1v22.2h-80.1q-4.4,0 -6.7,2.3 -2.3,2.3 -2.3,6.8v60q0,4.3 2.3,6.7 2.4,2.3 6.7,2.3z"
android:fillColor="#fff"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="633"
android:viewportHeight="586">
<path
android:pathData="m319.7,122.6v100.2q0,5.7 -1.5,10.4 -1.4,4.5 -3.8,8 -2.4,3.4 -5.6,5.9 -3.1,2.4 -6.7,3.9 -3.4,1.5 -7,2.3 -3.5,0.7 -6.6,0.7h-40.1q-5.7,0 -10.3,-1.5 -4.5,-1.4 -8,-3.8 -3.4,-2.5 -5.9,-5.6 -2.4,-3.2 -3.9,-6.6 -1.6,-3.5 -2.3,-7 -0.7,-3.6 -0.7,-6.7v-30.1q0,-8.5 3.1,-14.4 3.1,-6 7.7,-9.7 4.7,-3.7 10.2,-5.3 5.5,-1.7 10.1,-1.7h40.1v22.2h-39.9q-4.5,0 -6.8,2.4 -2.3,2.2 -2.3,6.5v29.9q0,4.6 2.2,6.9 2.3,2.3 6.7,2.3h40.1q4.5,0 6.7,-2.3 2.2,-2.3 2.2,-6.7v-100.2zM363.5,122.6v20.1h-22.2v-20.1zM363.5,161.6v101.3q0,5.7 -1.5,10.3 -1.4,4.6 -3.8,8.1 -2.4,3.4 -5.6,5.9 -3.1,2.3 -6.6,3.9 -3.5,1.5 -7,2.3 -3.5,0.7 -6.6,0.7h-40.1v-22.2h40.1q4.4,0 6.6,-2.3 2.3,-2.4 2.3,-6.7v-101.3zM407.3,122.6v20.1h-22.2v-20.1zM407.3,161.6v92.4h-22.2v-92.4zM232.4,391.5v22.4h-70.2v-22.4zM242.3,341.5v22.2h-89.1v100.3h-22.2v-111.3q0,-2.3 0.9,-4.4 0.8,-2 2.3,-3.6 1.5,-1.5 3.6,-2.4 2,-0.8 4.4,-0.8zM370,441.8v22.2h-80.2q-3.1,0 -6.6,-0.7 -3.6,-0.8 -7.1,-2.3 -3.4,-1.5 -6.5,-3.9 -3.2,-2.5 -5.7,-5.9 -2.4,-3.5 -3.8,-8 -1.5,-4.7 -1.5,-10.4v-60.1q0,-3.1 0.7,-6.6 0.8,-3.6 2.3,-7 1.6,-3.5 4,-6.7 2.5,-3.2 5.9,-5.6 3.5,-2.4 8.1,-3.9 4.5,-1.4 10.2,-1.4h80.2v22.2h-80.2q-4.3,0 -6.6,2.3 -2.4,2.3 -2.4,6.8v60q0,4.3 2.4,6.7 2.3,2.3 6.6,2.3zM499.8,441.8v22.2h-80.1q-3.1,0 -6.7,-0.7 -3.5,-0.8 -7,-2.3 -3.4,-1.5 -6.6,-3.9 -3.2,-2.5 -5.6,-5.9 -2.4,-3.5 -3.9,-8 -1.4,-4.7 -1.4,-10.4v-60.1q0,-3.1 0.7,-6.6 0.7,-3.6 2.3,-7 1.5,-3.5 4,-6.7 2.5,-3.2 5.9,-5.6 3.5,-2.4 8,-3.9 4.5,-1.4 10.3,-1.4h80.1v22.2h-80.1q-4.4,0 -6.7,2.3 -2.3,2.3 -2.3,6.8v60q0,4.3 2.3,6.7 2.4,2.3 6.7,2.3z"
android:fillColor="#fff"/>
</vector>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/icon_background"/>
<foreground android:drawable="@drawable/icon_foreground"/>
<monochrome android:drawable="@drawable/icon_foreground"/>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/icon_background"/>
<foreground android:drawable="@drawable/icon_foreground"/>
<monochrome android:drawable="@drawable/icon_foreground"/>
</adaptive-icon>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/icon_background"/>
<foreground android:drawable="@drawable/icon_foreground"/>
<monochrome android:drawable="@drawable/icon_foreground"/>
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/icon_background"/>
<foreground android:drawable="@drawable/icon_foreground"/>
<monochrome android:drawable="@drawable/icon_foreground"/>
</adaptive-icon>

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -1,3 +1,3 @@
<resources>
<string name="app_name">DJI FCC</string>
<resources>
<string name="app_name">DJI FCC</string>
</resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Djiffchack" parent="android:Theme.Material.Light.NoActionBar" />
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Djiffchack" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -1,19 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device
product-id="4128"
vendor-id="11427"/>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device
product-id="4128"
vendor-id="11427"/>
</resources>

View File

@ -1,17 +1,17 @@
package ch.mathieubroillet.djiffchack
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
package ch.mathieubroillet.djiffchack
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}

84
dji_fcc_patch.py Normal file
View File

@ -0,0 +1,84 @@
# DJI FCC Patch Script for Windows
#
# This script sends a "magic packet" to a DJI controller
# to enable FCC mode, which can increase the drone's signal range.
#
# Credits:
# - Based on the Android app by Mathieu Broillet: https://github.com/M4TH1EU/DJI-FCC-HACK
# - Original research by @galbb on MavicPilots.com
#
# HOW TO USE:
# 1. Install Python from the Microsoft Store or python.org.
# 2. Install the pyserial library by opening a Command Prompt or PowerShell and running:
# pip install pyserial
# 3. Turn on your drone and controller, and wait for them to connect.
# 4. Connect your PC to the bottom USB port of the controller.
# 5. Run this script.
# 6. If successful, disconnect the PC and connect your phone to the top USB port of the controller.
#
# DISCLAIMER: Use at your own risk. This script modifies your device's operation.
# The authors are not responsible for any damage or legal issues that may arise.
import serial
import serial.tools.list_ports
import time
# DJI Controller USB identifiers
VENDOR_ID = 11427
PRODUCT_ID = 4128
# The "magic bytes" that enable FCC mode
# (from ch.mathieubroillet.djiffchack.Constants.kt)
BYTES_1 = bytes([85, 13, 4, 33, 42, 31, 0, 0, 0, 0, 1, -122 & 0xFF, 32])
BYTES_2 = bytes([85, 24, 4, 32, 2, 9, 0, 0, 64, 9, 39, 0, 2, 72, 0, -1 & 0xFF, -1 & 0xFF, 2, 0, 0, 0, 0, -127 & 0xFF, 31])
def find_dji_controller():
"""Finds the COM port of the connected DJI controller."""
ports = serial.tools.list_ports.comports()
for port in ports:
if port.vid == VENDOR_ID and port.pid == PRODUCT_ID:
print(f"Found DJI Controller at {port.device}")
return port.device
return None
def send_patch(com_port):
"""Opens the serial port and sends the patch bytes."""
try:
with serial.Serial(com_port, baudrate=19200, bytesize=8, parity='N', stopbits=1, timeout=1) as ser:
print("Sending patch...")
# Send first byte array
ser.write(BYTES_1)
print(f"Sent {len(BYTES_1)} bytes (Packet 1).")
time.sleep(0.1) # Small delay between writes
# Send second byte array
ser.write(BYTES_2)
print(f"Sent {len(BYTES_2)} bytes (Packet 2).")
print("\nPatch sent successfully!")
print("You can now disconnect the controller from your PC.")
return True
except serial.SerialException as e:
print(f"Error: Could not open or write to serial port {com_port}.")
print(f"Details: {e}")
print("\nPlease ensure the controller is properly connected and no other software is using the COM port.")
return False
if __name__ == "__main__":
print("--- DJI FCC Patch Script ---")
print("Searching for DJI Controller...")
controller_port = find_dji_controller()
if controller_port:
send_patch(controller_port)
else:
print("\nDJI Controller not found.")
print("Please make sure:")
print("1. The controller is turned on and connected to the drone.")
print("2. Your PC is connected to the BOTTOM USB port of the controller.")
print("3. The necessary USB drivers are installed.")
print("\nPress Enter to exit.")
input()

View File

@ -1,23 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

View File

@ -1,33 +1,33 @@
[versions]
agp = "8.11.0"
kotlin = "2.0.0"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
[versions]
agp = "8.11.0"
kotlin = "2.0.0"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version = "8.6.0" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

5
gradlew vendored
View File

@ -174,7 +174,8 @@ fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
for i do printf %s\\n "$i" | sed "s/'/'\\''/g;1s/^/'/;
" ; done
echo " "
}
APP_ARGS=`save "$@"`
@ -182,4 +183,4 @@ APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
exec "$JAVACMD" "$@"

View File

@ -1,24 +1,24 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io")
}
}
rootProject.name = "dji-ffc-hack"
include(":app")
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io")
}
}
rootProject.name = "dji-ffc-hack"
include(":app")