M0: project scaffold (Gradle 9.3.1 / AGP 9.1.0, Compose shell, storage permissions)

This commit is contained in:
2026-10-14 14:55:10 +02:00
commit adfd95fc38
34 changed files with 1257 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!--
Storage access.
This app is a file manager, which Google Play lists as a permitted use of the
"All files access" special permission. The permission is declared here and must
be accompanied by a Permissions Declaration Form in the Play Console before the
release goes live (see docs/PLAY_RELEASE_CHECKLIST.md).
When the user does not grant it, the app falls back to the Storage Access
Framework (ACTION_OPEN_DOCUMENT_TREE) — see data/SafSource.
-->
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<!--
Media access, used for thumbnails and the built-in viewers on Android 13+.
On Android 12 and below this collapses to READ_EXTERNAL_STORAGE.
-->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- Android 14+ partial ("Selected photos") access. -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<!--
Needed only on Android 10 (API 29) and below to write through the legacy
storage path. Android 11+ writes go through the all-files-access route or SAF.
-->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
<application
android:name=".FileManagerApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.FileManager"
tools:targetApi="33">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.FileManager">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,13 @@
package de.softwareapp_hb.filemanager
import android.app.Application
/**
* Application entry point.
*
* Deliberately lightweight: the app has no dependency-injection graph, no analytics
* and no network access. From M2 this will own the browser/operations controller so
* that navigation and long-running file operations survive activity recreation; the
* storage layer and preference stores are opened lazily where they are used.
*/
class FileManagerApplication : Application()
@@ -0,0 +1,29 @@
package de.softwareapp_hb.filemanager
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import de.softwareapp_hb.filemanager.ui.browser.BrowserScreen
import de.softwareapp_hb.filemanager.ui.theme.FileManagerTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
FileManagerTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
BrowserScreen()
}
}
}
}
}
@@ -0,0 +1,56 @@
package de.softwareapp_hb.filemanager.ui.browser
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.softwareapp_hb.filemanager.R
/**
* The browser screen.
*
* M0 scaffold: it establishes the Compose shell (edge-to-edge scaffold, themed top
* bar, empty state) that the storage layer and file list will be wired into in M2/M4.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BrowserScreen(modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBar(title = { Text(text = stringResource(R.string.app_name)) })
},
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
text = stringResource(R.string.empty_state_title),
style = MaterialTheme.typography.titleLarge,
)
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(R.string.empty_state_body),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
@@ -0,0 +1,27 @@
package de.softwareapp_hb.filemanager.ui.theme
import androidx.compose.ui.graphics.Color
// Brand palette: a calm blue ("files and folders") accent with a teal secondary.
// Only the roles the app actually uses are overridden; the rest come from the
// Material 3 baseline, which keeps the scheme consistent.
val BluePrimaryLight = Color(0xFF1565C0)
val BlueOnPrimaryLight = Color(0xFFFFFFFF)
val BlueContainerLight = Color(0xFFD6E3FF)
val BlueOnContainerLight = Color(0xFF001B3D)
val TealSecondaryLight = Color(0xFF00696E)
val SurfaceLight = Color(0xFFFFFBFE)
val SurfaceVariantLight = Color(0xFFE0E2EC)
val OnSurfaceLight = Color(0xFF1A1C1E)
val ErrorLight = Color(0xFFBA1A1A)
val BluePrimaryDark = Color(0xFFA8C8FF)
val BlueOnPrimaryDark = Color(0xFF00315F)
val BlueContainerDark = Color(0xFF004787)
val BlueOnContainerDark = Color(0xFFD6E3FF)
val TealSecondaryDark = Color(0xFF4CDADE)
val SurfaceDark = Color(0xFF111318)
val SurfaceVariantDark = Color(0xFF44474E)
val OnSurfaceDark = Color(0xFFE2E2E6)
val ErrorDark = Color(0xFFFFB4AB)
@@ -0,0 +1,64 @@
package de.softwareapp_hb.filemanager.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 LightColors = lightColorScheme(
primary = BluePrimaryLight,
onPrimary = BlueOnPrimaryLight,
primaryContainer = BlueContainerLight,
onPrimaryContainer = BlueOnContainerLight,
secondary = TealSecondaryLight,
background = SurfaceLight,
surface = SurfaceLight,
surfaceVariant = SurfaceVariantLight,
onSurface = OnSurfaceLight,
error = ErrorLight,
)
private val DarkColors = darkColorScheme(
primary = BluePrimaryDark,
onPrimary = BlueOnPrimaryDark,
primaryContainer = BlueContainerDark,
onPrimaryContainer = BlueOnContainerDark,
secondary = TealSecondaryDark,
background = SurfaceDark,
surface = SurfaceDark,
surfaceVariant = SurfaceVariantDark,
onSurface = OnSurfaceDark,
error = ErrorDark,
)
/**
* App theme. Uses Material You dynamic colour on Android 12+ when available and
* falls back to the branded blue scheme otherwise.
*/
@Composable
fun FileManagerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
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 -> DarkColors
else -> LightColors
}
MaterialTheme(
colorScheme = colorScheme,
typography = FileManagerTypography,
content = content,
)
}
@@ -0,0 +1,9 @@
package de.softwareapp_hb.filemanager.ui.theme
import androidx.compose.material3.Typography
/**
* The app sticks with the Material 3 default type scale; a dedicated object keeps
* a single place to tune typography later.
*/
val FileManagerTypography = Typography()
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="@color/ic_launcher_background"
android:pathData="M0,0 h108 v108 h-108 z" />
</vector>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Launcher foreground: a folder, with a second folder offset behind it for depth.
Drawn inside the 66dp safe zone of the 108dp adaptive-icon canvas.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Back folder (semi-transparent, offset up and to the right) -->
<path
android:fillAlpha="0.45"
android:fillColor="#FFFFFF"
android:pathData="M34,38 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z" />
<!-- Front folder -->
<path
android:fillColor="#FFFFFF"
android:pathData="M28,44 h14 l5,5 h27 a4,4 0 0 1 4,4 v20 a4,4 0 0 1 -4,4 h-46 a4,4 0 0 1 -4,-4 v-25 a4,4 0 0 1 4,-4 z" />
<!-- Document tab peeking out of the front folder -->
<path
android:fillColor="#1565C0"
android:pathData="M44,56 h20 a2,2 0 0 1 2,2 v12 a2,2 0 0 1 -2,2 h-20 a2,2 0 0 1 -2,-2 v-12 a2,2 0 0 1 2,-2 z" />
</vector>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Notification small icon. Monochrome on purpose: Android renders notification icons
as an alpha mask, so only the alpha channel matters.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,6.5 a1.5,1.5 0 0 1 1.5,-1.5 h4 l2,2 h9 a1.5,1.5 0 0 1 1.5,1.5 v9 a1.5,1.5 0 0 1 -1.5,1.5 h-15 a1.5,1.5 0 0 1 -1.5,-1.5 z" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="window_background">#111318</color>
<color name="ic_launcher_background">#1565C0</color>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.FileManager" parent="android:Theme.Material.NoActionBar">
<item name="android:windowBackground">@color/window_background</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>
</style>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="window_background">#FFFBFE</color>
<color name="ic_launcher_background">#1565C0</color>
</resources>
+16
View File
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!--
All user-visible text lives here. English is currently the only locale, but keeping the
text in resources is what makes a translation possible later at all — Play's automatic
translation covers the store listing, not the app UI. Plurals are declared properly so a
language with more than two plural forms needs no code change.
NOTE: `app_name` is a working title. The Play listing needs a distinctive name before
the first upload (it is changeable afterwards, unlike the applicationId).
-->
<string name="app_name">File Manager</string>
<!-- Browser empty state (M0 placeholder for the real file list). -->
<string name="empty_state_title">Your files, without the clutter</string>
<string name="empty_state_body">Ad-free, account-free and completely offline. Browsing is wired up next.</string>
</resources>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Compose owns the real theming; this XML theme only prevents a jarring flash
before the first frame. It is intentionally a framework theme so the app
needs no Material Components (View-system) dependency.
-->
<style name="Theme.FileManager" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:windowBackground">@color/window_background</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
</style>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Bookmarks, recents and settings are device-specific, so nothing this app stores is
worth carrying to another device or restoring into a fresh install.
-->
<full-backup-content>
<exclude domain="sharedpref" path="." />
<exclude domain="database" path="." />
</full-backup-content>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" path="." />
<exclude domain="database" path="." />
</cloud-backup>
<device-transfer>
<exclude domain="sharedpref" path="." />
<exclude domain="database" path="." />
</device-transfer>
</data-extraction-rules>