diff --git a/shot-android/src/main/java/com/karumi/shot/compose/ComposeScreenshot.kt b/shot-android/src/main/java/com/karumi/shot/compose/ComposeScreenshot.kt index a92e0020..f5db2094 100644 --- a/shot-android/src/main/java/com/karumi/shot/compose/ComposeScreenshot.kt +++ b/shot-android/src/main/java/com/karumi/shot/compose/ComposeScreenshot.kt @@ -1,23 +1,24 @@ package com.karumi.shot.compose import android.graphics.Bitmap -import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.ui.test.SemanticsNodeInteraction import com.google.gson.annotations.SerializedName +import com.karumi.shot.permissions.AndroidStoragePermissions class ComposeScreenshot( private val session: ScreenshotTestSession, - private val saver: ScreenshotSaver + private val saver: ScreenshotSaver, + private val permissions: AndroidStoragePermissions ) { fun saveScreenshot(bitmap: Bitmap, data: ScreenshotMetadata) { + permissions.checkPermissions() saver.saveScreenshot(ScreenshotToSave(ScreenshotSource.Bitmap(bitmap), data)) session.add(data) } - @RequiresApi(Build.VERSION_CODES.O) fun saveScreenshot(node: SemanticsNodeInteraction, data: ScreenshotMetadata) { + permissions.checkPermissions() saver.saveScreenshot(ScreenshotToSave(ScreenshotSource.Node(node), data)) session.add(data) } diff --git a/shot-android/src/main/java/com/karumi/shot/compose/ComposeScreenshotRunner.kt b/shot-android/src/main/java/com/karumi/shot/compose/ComposeScreenshotRunner.kt index 70043712..a686f1e0 100644 --- a/shot-android/src/main/java/com/karumi/shot/compose/ComposeScreenshotRunner.kt +++ b/shot-android/src/main/java/com/karumi/shot/compose/ComposeScreenshotRunner.kt @@ -1,6 +1,7 @@ package com.karumi.shot.compose import android.app.Instrumentation +import com.karumi.shot.permissions.AndroidStoragePermissions class ComposeScreenshotRunner { companion object { @@ -10,7 +11,8 @@ class ComposeScreenshotRunner { fun onCreate(instrumentation: Instrumentation) { composeScreenshot = ComposeScreenshot( session = ScreenshotTestSession(), - saver = ScreenshotSaver(instrumentation.context.packageName, SemanticsNodeBitmapGenerator()) + saver = ScreenshotSaver(instrumentation.context.packageName, SemanticsNodeBitmapGenerator()), + permissions = AndroidStoragePermissions(instrumentation) ) } diff --git a/shot-android/src/main/java/com/karumi/shot/permissions/AndroidStoragePermissions.java b/shot-android/src/main/java/com/karumi/shot/permissions/AndroidStoragePermissions.java new file mode 100644 index 00000000..471687da --- /dev/null +++ b/shot-android/src/main/java/com/karumi/shot/permissions/AndroidStoragePermissions.java @@ -0,0 +1,70 @@ +package com.karumi.shot.permissions; + +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.ParcelFileDescriptor; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; + +public class AndroidStoragePermissions { + private static final String WRITE_PERMISSION = "android.permission.WRITE_EXTERNAL_STORAGE"; + private static final String READ_PERMISSION = "android.permission.READ_EXTERNAL_STORAGE"; + private static final String[] REQUIRED_PERMISSIONS = + new String[]{WRITE_PERMISSION, READ_PERMISSION}; + + private final Instrumentation instrumentation; + + public AndroidStoragePermissions(Instrumentation instrumentation) { + this.instrumentation = instrumentation; + } + + public void checkPermissions() { + Context testAppContext = instrumentation.getContext(); + for (String permission : REQUIRED_PERMISSIONS) { + if ((permission.equals(READ_PERMISSION) && Build.VERSION.SDK_INT < 16) + || testAppContext.checkCallingOrSelfPermission(permission) + == PackageManager.PERMISSION_GRANTED) { + continue; + } + if (Build.VERSION.SDK_INT < 23) { + throw new RuntimeException("We need " + permission + " permission for screenshot tests"); + } + Context targetContext = instrumentation.getTargetContext(); + grantPermission(targetContext, permission); + grantPermission(testAppContext, permission); + } + } + + private void grantPermission(Context context, String permission) { + if (Build.VERSION.SDK_INT < 23) { + return; + } + UiAutomation automation = instrumentation.getUiAutomation(); + String command = + String.format(Locale.ENGLISH, "pm grant %s %s", context.getPackageName(), permission); + ParcelFileDescriptor pfd = automation.executeShellCommand(command); + InputStream stream = new FileInputStream(pfd.getFileDescriptor()); + try { + byte[] buffer = new byte[1024]; + while (stream.read(buffer) != -1) { + // Consume stdout to ensure the command completes + } + } catch (IOException ignored) { + } finally { + try { + stream.close(); + } catch (IOException ignored) { + } + try { + pfd.close(); + } catch (IOException ignored) { + } + } + } +} \ No newline at end of file diff --git a/shot-android/src/test/java/com/karumi/shot/compose/ComposeScreenshotTest.kt b/shot-android/src/test/java/com/karumi/shot/compose/ComposeScreenshotTest.kt index f0664d92..a250a026 100644 --- a/shot-android/src/test/java/com/karumi/shot/compose/ComposeScreenshotTest.kt +++ b/shot-android/src/test/java/com/karumi/shot/compose/ComposeScreenshotTest.kt @@ -1,6 +1,7 @@ package com.karumi.shot.compose import androidx.compose.ui.test.SemanticsNodeInteraction +import com.karumi.shot.permissions.AndroidStoragePermissions import com.nhaarman.mockito_kotlin.* import junit.framework.TestCase.assertEquals import org.junit.Before @@ -24,9 +25,12 @@ class ComposeScreenshotTest { @Mock private lateinit var node: SemanticsNodeInteraction + @Mock + private lateinit var permissions: AndroidStoragePermissions + @Before fun setUp() { - composeScreenshot = ComposeScreenshot(ScreenshotTestSession(), screenshotSaver) + composeScreenshot = ComposeScreenshot(ScreenshotTestSession(), screenshotSaver, permissions) } @Test @@ -39,6 +43,15 @@ class ComposeScreenshotTest { assertEquals(expectedSessionMetadata, composeScreenshot.saveMetadata().getScreenshotSessionMetadata()) } + @Test + fun grantsStoragePermissionsWhenSavingAnyMetadata() { + val data = anyScreenshotMetadata + + composeScreenshot.saveScreenshot(node, data) + + verify(permissions).checkPermissions() + } + @Test fun wheneverANewScreenshotIsSavedTheNodeAssociatedIsSavedIntoTheSdCard() { val data = anyScreenshotMetadata