Skip to content

Commit e291c3d

Browse files
committed
fix(mobile): comply with Android media permissions policy
1 parent efafb15 commit e291c3d

3 files changed

Lines changed: 58 additions & 5 deletions

File tree

apps/mobile/app.config.base.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,17 @@ export default ({ config }: ConfigContext): ExpoConfig => {
111111
},
112112
android: {
113113
package: "is.follow",
114+
// Media selection uses system pickers; saving only needs write access on older Android versions.
115+
blockedPermissions: [
116+
"android.permission.ACCESS_MEDIA_LOCATION",
117+
"android.permission.CAMERA",
118+
"android.permission.READ_EXTERNAL_STORAGE",
119+
"android.permission.READ_MEDIA_AUDIO",
120+
"android.permission.READ_MEDIA_IMAGES",
121+
"android.permission.READ_MEDIA_VIDEO",
122+
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED",
123+
"android.permission.RECORD_AUDIO",
124+
],
114125
adaptiveIcon: {
115126
foregroundImage: adaptiveIconPath,
116127
monochromeImage: adaptiveIconPath,
@@ -158,7 +169,8 @@ export default ({ config }: ConfigContext): ExpoConfig => {
158169
{
159170
photosPermission: "Allow $(PRODUCT_NAME) to access your photos.",
160171
savePhotosPermission: "Allow $(PRODUCT_NAME) to save photos.",
161-
isAccessMediaLocationEnabled: true,
172+
isAccessMediaLocationEnabled: false,
173+
granularPermissions: [],
162174
},
163175
],
164176
"expo-apple-authentication",
@@ -192,6 +204,8 @@ export default ({ config }: ConfigContext): ExpoConfig => {
192204
"expo-image-picker",
193205
{
194206
photosPermission: "Allow $(PRODUCT_NAME) to access your photos.",
207+
cameraPermission: false,
208+
microphonePermission: false,
195209
},
196210
],
197211
[

apps/mobile/scripts/app-config.base.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from "vitest"
22

3-
import { resolveRuntimeVersion } from "../app.config.base"
3+
import createExpoConfig, { resolveRuntimeVersion } from "../app.config.base"
44

55
describe("resolveRuntimeVersion", () => {
66
it("keeps the development runtime version stable", () => {
@@ -33,3 +33,42 @@ describe("resolveRuntimeVersion", () => {
3333
).toThrow(/OTA_RUNTIME_VERSION/i)
3434
})
3535
})
36+
37+
describe("Android media permissions", () => {
38+
const config = createExpoConfig({
39+
config: {},
40+
} as Parameters<typeof createExpoConfig>[0])
41+
42+
it("blocks broad media access that is not required by the app", () => {
43+
expect(config.android?.blockedPermissions).toEqual([
44+
"android.permission.ACCESS_MEDIA_LOCATION",
45+
"android.permission.CAMERA",
46+
"android.permission.READ_EXTERNAL_STORAGE",
47+
"android.permission.READ_MEDIA_AUDIO",
48+
"android.permission.READ_MEDIA_IMAGES",
49+
"android.permission.READ_MEDIA_VIDEO",
50+
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED",
51+
"android.permission.RECORD_AUDIO",
52+
])
53+
})
54+
55+
it("configures media APIs for picker and write-only access", () => {
56+
expect(config.plugins).toContainEqual([
57+
"expo-media-library",
58+
{
59+
photosPermission: "Allow $(PRODUCT_NAME) to access your photos.",
60+
savePhotosPermission: "Allow $(PRODUCT_NAME) to save photos.",
61+
isAccessMediaLocationEnabled: false,
62+
granularPermissions: [],
63+
},
64+
])
65+
expect(config.plugins).toContainEqual([
66+
"expo-image-picker",
67+
{
68+
photosPermission: "Allow $(PRODUCT_NAME) to access your photos.",
69+
cameraPermission: false,
70+
microphonePermission: false,
71+
},
72+
])
73+
})
74+
})

apps/mobile/src/components/ui/image/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { IMAGE_PROXY_URL } from "@follow/utils/img-proxy"
33
import ImageEditor from "@react-native-community/image-editor"
44
import * as FileSystem from "expo-file-system/legacy"
55
import type { ImageProps, ImageSource } from "expo-image"
6-
import { saveToLibraryAsync, usePermissions } from "expo-media-library"
6+
import { Asset, usePermissions } from "expo-media-library"
77
import * as Sharing from "expo-sharing"
88
import { useCallback } from "react"
99
import { Image } from "react-native"
@@ -145,7 +145,7 @@ export const saveImageToMediaLibrary = async ({ uri }: { uri: string }) => {
145145
const croppedImage = await getImageData(uri)
146146
const filename = `${extractFilenameFromUrl(uri)}.png`
147147
const { filePath, cleanup } = await createTempFile(croppedImage.base64, filename)
148-
await saveToLibraryAsync(filePath)
148+
await Asset.create(filePath)
149149
cleanup()
150150
}
151151

@@ -161,7 +161,7 @@ export const saveImageToMediaLibrary = async ({ uri }: { uri: string }) => {
161161
*/
162162
export function useSaveImageToMediaLibrary() {
163163
const [permissionResponse, requestPermission, getPermission] = usePermissions({
164-
granularPermissions: ["photo"],
164+
writeOnly: true,
165165
})
166166
return useCallback(
167167
async (uri: string) => {

0 commit comments

Comments
 (0)