M0: project scaffold (Gradle 9.3.1 / AGP 9.1.0, Compose shell, storage permissions)
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user