diff --git a/shared/android/app/build.gradle b/shared/android/app/build.gradle index 2c3402c31031..71df9ac3d9f2 100644 --- a/shared/android/app/build.gradle +++ b/shared/android/app/build.gradle @@ -92,11 +92,11 @@ def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBu def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' android { - ndkVersion rootProject.ext.ndkVersion + ndkVersion = rootProject.ext.ndkVersion buildToolsVersion rootProject.ext.buildToolsVersion compileSdk rootProject.ext.compileSdkVersion - namespace "io.keybase.ossifrage" + namespace = "io.keybase.ossifrage" defaultConfig { applicationId "io.keybase.ossifrage" minSdkVersion rootProject.ext.minSdkVersion @@ -105,7 +105,7 @@ android { versionName VERSION_NAME buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" // KB added - multiDexEnabled true + multiDexEnabled = true } signingConfigs { @@ -124,14 +124,14 @@ android { } } release { - signingConfig signingConfigs.release + signingConfig = signingConfigs.release minifyEnabled enableMinifyInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } releaseUnsigned.initWith(buildTypes.release) releaseUnsigned { applicationIdSuffix ".unsigned" - signingConfig buildTypes.debug.signingConfig + signingConfig = buildTypes.debug.signingConfig matchingFallbacks = ['release'] manifestPlaceholders["usesCleartextTraffic"] = "false" } @@ -165,11 +165,11 @@ dependencies { implementation jscFlavor } - implementation 'androidx.work:work-runtime:2.11.1' + implementation 'androidx.work:work-runtime:2.11.2' implementation 'androidx.multidex:multidex:2.0.1' - implementation "com.google.firebase:firebase-messaging:25.0.1" + implementation "com.google.firebase:firebase-messaging:25.0.2" implementation "com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}" - implementation 'org.msgpack:msgpack-core:0.9.10' + implementation 'org.msgpack:msgpack-core:0.9.12' implementation project(':keybaselib') implementation 'com.android.installreferrer:installreferrer:2.2' implementation "androidx.lifecycle:lifecycle-common-java8:2.10.0" diff --git a/shared/android/build.gradle b/shared/android/build.gradle index a322cb3cfcea..405df5f083f1 100644 --- a/shared/android/build.gradle +++ b/shared/android/build.gradle @@ -35,21 +35,21 @@ allprojects { repositories { maven { // expo-camera bundles a custom com.google.android:cameraview - url "$rootDir/../node_modules/expo-camera/android/maven" + url = "$rootDir/../node_modules/expo-camera/android/maven" } maven { // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm - url(reactNativeAndroidDir) + url = reactNativeAndroidDir } maven { // Android JSC is installed from npm - url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist')) + url = new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist') } google() mavenCentral() - maven { url 'https://www.jitpack.io' } + maven { url = 'https://www.jitpack.io' } } } diff --git a/shared/app/index.native.tsx b/shared/app/index.native.tsx index 88a89f640e08..8c035b3ba790 100644 --- a/shared/app/index.native.tsx +++ b/shared/app/index.native.tsx @@ -7,7 +7,7 @@ import * as React from 'react' import Main from './main' import {KeyboardProvider} from 'react-native-keyboard-controller' import Animated, {ReducedMotionConfig, ReduceMotion} from 'react-native-reanimated' -import {AppRegistry, AppState, Appearance, Keyboard} from 'react-native' +import {AppRegistry, AppState, Appearance, Keyboard, Platform} from 'react-native' import {PortalProvider} from '@/common-adapters/portal.native' import {SafeAreaProvider, initialWindowMetrics} from 'react-native-safe-area-context' import {makeEngine} from '../engine' @@ -31,7 +31,10 @@ setServiceDecoration(ServiceDecoration) // SDWebImage (used by expo-image) flushes its memory cache on iOS memory warnings, but // the simulator never sends memory warnings. Cap the cache so loading hundreds of chat // images doesn't exhaust VM in the simulator. On a real device this is a safety net only. -ExpoImage.configureCache({maxMemoryCost: 100 * 1024 * 1024}) +// configureCache is iOS-only native (no Android impl) so calling it on Android throws. +if (Platform.OS === 'ios') { + ExpoImage.configureCache({maxMemoryCost: 100 * 1024 * 1024}) +} module.hot?.accept(() => { console.log('accepted update in shared/index.native') diff --git a/shared/fs/banner/conflict-banner.tsx b/shared/fs/banner/conflict-banner.tsx index 499930ccc75b..4b697be08bed 100644 --- a/shared/fs/banner/conflict-banner.tsx +++ b/shared/fs/banner/conflict-banner.tsx @@ -27,7 +27,7 @@ const ConnectedBanner = (ownProps: OwnProps) => { C.ignorePromise(f()) } const onGoToSamePathInDifferentTlf = (tlfPath: T.FS.Path) => { - C.Router2.navigateAppend({name: 'fsRoot', params: {path: FS.rebasePathToDifferentTlf(path, tlfPath)}}) + C.Router2.navigateAppend({name: 'fsBrowse', params: {path: FS.rebasePathToDifferentTlf(path, tlfPath)}}) } const onHelp = () => { void openUrl('https://book.keybase.io/docs/files/details#conflict-resolution') diff --git a/shared/fs/banner/public-reminder.tsx b/shared/fs/banner/public-reminder.tsx index 6445b6b9d343..30376433908a 100644 --- a/shared/fs/banner/public-reminder.tsx +++ b/shared/fs/banner/public-reminder.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import * as Kb from '@/common-adapters' import * as T from '@/constants/types' -import {navigateAppend} from '@/constants/router' +import {getVisibleScreen, navigateAppend} from '@/constants/router' import {useFsPathItem} from '@/fs/common' import * as FS from '@/constants/fs' @@ -22,8 +22,13 @@ const PublicBanner = (props: Props) => { const isWritable = useFsPathItem(path).writable const lastPublicBannerClosedTlf = props.lastClosedTlf ?? '' const setLastPublicBannerClosedTlf = React.useCallback( - (tlf: string) => - navigateAppend({name: 'fsRoot', params: {lastClosedPublicBannerTlf: tlf, path}}, true), + (tlf: string) => { + // Dismiss = update the param on the screen we're on. The public folder may + // be the Files tab root (fsRoot) or a pushed folder (fsBrowse); replace + // only collapses to setParams when the name matches the current route. + const name = getVisibleScreen()?.name === 'fsBrowse' ? 'fsBrowse' : 'fsRoot' + navigateAppend({name, params: {lastClosedPublicBannerTlf: tlf, path}}, true) + }, [path] ) diff --git a/shared/fs/banner/reset-banner.tsx b/shared/fs/banner/reset-banner.tsx index b4c4bf5731f1..c3be5ebead02 100644 --- a/shared/fs/banner/reset-banner.tsx +++ b/shared/fs/banner/reset-banner.tsx @@ -4,7 +4,6 @@ import {folderNameWithoutUsers} from '@/util/kbfs' import * as Kb from '@/common-adapters' import * as RowTypes from '@/fs/browser/rows/types' import {useFsErrorActionOrThrow, useFsTlf} from '@/fs/common' -import * as FS from '@/constants/fs' import {navToProfile} from '@/constants/router' type OwnProps = {path: T.FS.Path} @@ -25,7 +24,7 @@ const ConnectedBanner = (ownProps: OwnProps) => { }, {}) const filteredPathName = folderNameWithoutUsers(pathElems[2] ?? '', users) const filteredPath = T.FS.stringToPath(['', pathElems[0], pathElems[1], filteredPathName].join('/')) - FS.navToPath(filteredPath) + C.Router2.navigateAppend({name: 'fsBrowse', params: {path: filteredPath}}) } const onReAddToTeam = (username: string) => () => { if (!tlf.teamId) return diff --git a/shared/fs/browser/destination-picker.tsx b/shared/fs/browser/destination-picker.tsx index b6a6872d8893..88e6af9d79b4 100644 --- a/shared/fs/browser/destination-picker.tsx +++ b/shared/fs/browser/destination-picker.tsx @@ -97,14 +97,14 @@ const ConnectedDestinationPicker = (ownProps: OwnProps) => { ? () => { moveOrCopy('copy') clearModals() - nav.safeNavigateAppend({name: 'fsRoot', params: {path: parentPath}}) + nav.safeNavigateAppend({name: 'fsBrowse', params: {path: parentPath}}) } : undefined const onMoveHere = isMovable ? () => { moveOrCopy('move') clearModals() - nav.safeNavigateAppend({name: 'fsRoot', params: {path: parentPath}}) + nav.safeNavigateAppend({name: 'fsBrowse', params: {path: parentPath}}) } : undefined const onNewFolder = diff --git a/shared/fs/common/daemon.tsx b/shared/fs/common/daemon.tsx index 7bc545e81e9c..d7a8dc7ad515 100644 --- a/shared/fs/common/daemon.tsx +++ b/shared/fs/common/daemon.tsx @@ -12,7 +12,7 @@ type FsDaemonActions = { onlineStatusChanged: (onlineStatus: T.RPCGen.KbfsOnlineStatus) => void } -const fsRouteNames: ReadonlyArray = ['fsRoot', 'fsFilePreview'] +const fsRouteNames: ReadonlyArray = ['fsRoot', 'fsBrowse', 'fsFilePreview'] const emptyFsDaemonActions: FsDaemonActions = { checkKbfsDaemonRpcStatus: () => {}, diff --git a/shared/fs/common/use-open.tsx b/shared/fs/common/use-open.tsx index 85c922c12a81..bd7c1fb400f2 100644 --- a/shared/fs/common/use-open.tsx +++ b/shared/fs/common/use-open.tsx @@ -22,7 +22,7 @@ export const useOpen = (props: Props) => { }) } else { nav.safeNavigateAppend({ - name: 'fsRoot', + name: 'fsBrowse', params: {initialLastModifiedTimestamp: knownTimestamp, initialPathType: knownType, path: props.path}, }) } diff --git a/shared/fs/routes.tsx b/shared/fs/routes.tsx index c927f060b081..ab9e02cbe9fd 100644 --- a/shared/fs/routes.tsx +++ b/shared/fs/routes.tsx @@ -68,6 +68,25 @@ const destPickerDesktopHeaderStyle = Kb.Styles.padding( ) const noShrinkStyle = {flexShrink: 0} as const +// Options shared by fsRoot (the Files tab root) and fsBrowse (a folder pushed on +// top). They render the same screen; the only difference is where each lives in +// the navigator tree (see fsBrowse below). +const fsFolderGetOptions = (ownProps?: {route: {params?: {path?: T.FS.Path}}}) => { + // strange edge case where the root can actually have no params + const params = ownProps?.route.params + const path = params?.path ?? FS.defaultPath + return isMobile + ? { + header: () => , + } + : { + headerRightActions: () => {}} />, + headerTitle: () => , + subHeader: MainBanner, + title: path === FS.defaultPath ? 'Files' : T.FS.getPathName(path), + } +} + export const newRoutes = defineRouteMap({ fsFilePreview: C.makeScreen(FsFilePreview, { getOptions: (ownProps?) => { @@ -84,23 +103,13 @@ export const newRoutes = defineRouteMap({ } }, }), - fsRoot: C.makeScreen(FsRoot, { - getOptions: (ownProps?) => { - // strange edge case where the root can actually have no params - const params = ownProps?.route.params - const path = params?.path ?? FS.defaultPath - return isMobile - ? { - header: () => <MobileHeader path={path} />, - } - : { - headerRightActions: () => <Actions path={path} onTriggerFilterMobile={() => {}} />, - headerTitle: () => <Title path={path} />, - subHeader: MainBanner, - title: path === FS.defaultPath ? 'Files' : T.FS.getPathName(path), - } - }, - }), + fsRoot: C.makeScreen(FsRoot, {getOptions: fsFolderGetOptions}), + // Same screen as fsRoot, but used when drilling into a folder. fsRoot is the + // Files tab root (lives inside the tab stack); fsBrowse is not a tab root, so + // on phones it lands in the app root stack and renders above the bottom tab + // bar — hiding it on push, matching every other tab. Open a folder via + // fsBrowse; jump into the Files tab from elsewhere via fsRoot. + fsBrowse: C.makeScreen(FsRoot, {getOptions: fsFolderGetOptions}), }) export const newModalRoutes = defineRouteMap({ diff --git a/shared/fs/simple-screens/oops.tsx b/shared/fs/simple-screens/oops.tsx index 547c6bc22942..07f2f293f00a 100644 --- a/shared/fs/simple-screens/oops.tsx +++ b/shared/fs/simple-screens/oops.tsx @@ -99,7 +99,7 @@ const NonExistent = (props: Props) => ( const Oops = (props: OwnProps) => { const nav = useSafeNavigation() const openParent = () => - nav.safeNavigateAppend({name: 'fsRoot', params: {path: T.FS.getPathParent(props.path)}}) + nav.safeNavigateAppend({name: 'fsBrowse', params: {path: T.FS.getPathParent(props.path)}}) switch (props.reason) { case T.FS.SoftError.NoAccess: return <NoAccess {...props} openParent={openParent} /> diff --git a/shared/package.json b/shared/package.json index ccdd3d7ff4a4..afe14f554606 100644 --- a/shared/package.json +++ b/shared/package.json @@ -10,7 +10,7 @@ "android:bundle:prod": "curl -o ~/Desktop/bundle.prod.js 'http://localhost:8081/index.bundle?platform=android&dev=false&minify=false&app=io.keybase.ossifrage&modulesOnly=false&runModule=true'", "android:bundle:profile": "NODE_OPTIONS=--no-experimental-fetch npx react-native profile-hermes ~/Desktop/", "android:clean": "cd android; rm -rf build; rm -rf app/.cxx ; ./gradlew clean", - "android:debug": "NODE_ENV=development npx expo run:android --variant debug", + "android:debug": "NODE_ENV=development ./scripts/android-debug.sh", "android:gobuild": "./react-native/gobuild.sh android && yarn postinstall", "android:jsbundle": "mkdir -p android/dist && react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/dist/main.jsbundle --sourcemap-output android/dist/main.jsbundle.sourcemap", "android:logs:clear": "adb logcat -b all -c", diff --git a/shared/router-v2/screen-layout.tsx b/shared/router-v2/screen-layout.tsx index 365141eb2f2d..705b2b4e9c3c 100644 --- a/shared/router-v2/screen-layout.tsx +++ b/shared/router-v2/screen-layout.tsx @@ -36,11 +36,22 @@ const TabScreenWrapper = ({children}: {children: React.ReactNode}) => { ) } -const StackScreenWrapper = ({children}: {children: React.ReactNode}) => ( - <Kb.Box2 direction="vertical" fullWidth={true} style={styles.tabScreen}> - {children} - </Kb.Box2> -) +const StackScreenWrapper = ({children}: {children: React.ReactNode}) => { + // Android targets SDK 35+ which enforces edge-to-edge, so content draws under + // the system nav bar unless we apply the bottom inset ourselves. + if (isAndroid) { + return ( + <RNScreensSafeAreaView edges={{bottom: true}} style={styles.tabScreen}> + {children} + </RNScreensSafeAreaView> + ) + } + return ( + <Kb.Box2 direction="vertical" fullWidth={true} style={styles.tabScreen}> + {children} + </Kb.Box2> + ) +} const desktopMakeLayout = ( isModal: boolean, diff --git a/shared/scripts/android-debug.sh b/shared/scripts/android-debug.sh new file mode 100755 index 000000000000..30b395a0fd49 --- /dev/null +++ b/shared/scripts/android-debug.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Build + install + launch the Android debug app, skipping `expo run:android`'s +# dev-client deep link (keybase://expo-development-client/?url=...). That link is +# only handled by expo-dev-client, which this app does not bundle, so it 404s. +# A plain debug build connects to Metro at localhost:8081 via `adb reverse`, so +# we launch MainActivity directly instead. +set -euo pipefail + +cd "$(dirname "$0")/.." # shared/ + +PKG=io.keybase.ossifrage +APK=android/app/build/outputs/apk/debug/app-debug.apk + +# Build debug APK. +(cd android && ./gradlew :app:assembleDebug) + +# Install on the connected device/emulator. +adb install -r -d "$APK" + +# Route the device's localhost:8081 to the host Metro server. +adb reverse tcp:8081 tcp:8081 + +# Start Metro in the background; kill it when this script exits. +npx expo start & +METRO_PID=$! +trap 'kill "$METRO_PID" 2>/dev/null || true' EXIT + +# Wait for Metro to answer before launching, else the app red-screens. +until curl -fs http://localhost:8081/status >/dev/null 2>&1; do sleep 1; done + +# Launch the app directly (no deep link). +adb shell am start -n "$PKG/.MainActivity" + +# Keep Metro in the foreground. +wait "$METRO_PID"