From 9f3e871637ef037c0f32a9da5a0af5f0acc763f8 Mon Sep 17 00:00:00 2001 From: Percy Wegmann Date: Tue, 26 Mar 2024 17:54:14 -0500 Subject: [PATCH] android/ui: prompt for write external storage permission and show error if necessary Updates #ENG-2948 Signed-off-by: Percy Wegmann --- android/build.gradle | 1 + .../src/main/java/com/tailscale/ipn/App.kt | 15 ------- .../java/com/tailscale/ipn/IPNActivity.java | 14 ------ .../com/tailscale/ipn/ui/view/ErrorDialog.kt | 2 +- .../com/tailscale/ipn/ui/view/MainView.kt | 44 +++++++++++++++++++ android/src/main/res/values/strings.xml | 3 ++ 6 files changed, 49 insertions(+), 30 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 0240870..155b72c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -87,6 +87,7 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' implementation 'androidx.activity:activity-compose:1.8.2' + implementation "com.google.accompanist:accompanist-permissions:0.34.0" // Navigation dependencies. def nav_version = "2.7.7" diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index d8f0924..1536d92 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -36,7 +36,6 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.tailscale.ipn.mdm.AlwaysNeverUserDecidesSetting @@ -314,20 +313,6 @@ class App : Application(), libtailscale.AppContext { return null } - fun requestWriteStoragePermission(act: Activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || - Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - // We can write files without permission. - return - } - if (ContextCompat.checkSelfPermission(act, Manifest.permission.WRITE_EXTERNAL_STORAGE) == - PackageManager.PERMISSION_GRANTED) { - return - } - act.requestPermissions( - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), IPNActivity.WRITE_STORAGE_RESULT) - } - @Throws(IOException::class) fun insertMedia(name: String?, mimeType: String): String { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { diff --git a/android/src/main/java/com/tailscale/ipn/IPNActivity.java b/android/src/main/java/com/tailscale/ipn/IPNActivity.java index c5eb741..48792eb 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNActivity.java +++ b/android/src/main/java/com/tailscale/ipn/IPNActivity.java @@ -5,7 +5,6 @@ package com.tailscale.ipn; import android.app.Activity; import android.content.Intent; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.Cursor; import android.net.Uri; @@ -14,11 +13,7 @@ import android.provider.OpenableColumns; import java.util.List; -import libtailscale.Libtailscale; - public final class IPNActivity extends Activity { - final static int WRITE_STORAGE_RESULT = 1000; - @Override public void onCreate(Bundle state) { super.onCreate(state); @@ -88,15 +83,6 @@ public final class IPNActivity extends Activity { // App.onShareIntent(nfiles, types, mimes, items, names, sizes); } - @Override - public void onRequestPermissionsResult(int reqCode, String[] perms, int[] grants) { - if (reqCode == WRITE_STORAGE_RESULT) { - if (grants.length > 0 && grants[0] == PackageManager.PERMISSION_GRANTED) { - Libtailscale.onWriteStorageGranted(); - } - } - } - @Override public void onDestroy() { super.onDestroy(); diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/ErrorDialog.kt b/android/src/main/java/com/tailscale/ipn/ui/view/ErrorDialog.kt index 4065dbd..7e5771b 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/ErrorDialog.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/ErrorDialog.kt @@ -37,7 +37,7 @@ fun ErrorDialog(type: ErrorDialogType, action: () -> Unit = {}) { @Composable fun ErrorDialog( - @StringRes title: Int, + @StringRes title: Int = R.string.error, @StringRes message: Int, @StringRes buttonText: Int = R.string.ok, onDismiss: () -> Unit = {} diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index e536e73..9180a08 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -3,6 +3,9 @@ package com.tailscale.ipn.ui.view +import android.Manifest +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -36,8 +39,10 @@ import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -49,6 +54,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale import com.tailscale.ipn.R import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.IpnLocal @@ -60,6 +69,7 @@ import com.tailscale.ipn.ui.util.PeerSet import com.tailscale.ipn.ui.util.flag import com.tailscale.ipn.ui.util.itemsWithDividers import com.tailscale.ipn.ui.viewModel.MainViewModel +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow // Navigation actions for the MainView @@ -69,6 +79,7 @@ data class MainViewNavigation( val onNavigateToExitNodes: () -> Unit ) +@OptIn(ExperimentalPermissionsApi::class) @Composable fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewModel()) { LoadingIndicator.Wrap { @@ -108,6 +119,8 @@ fun MainView(navigation: MainViewNavigation, viewModel: MainViewModel = viewMode when (state.value) { Ipn.State.Running -> { + PromptWriteStoragePermissionsIfNecessary() + val selfPeerId = viewModel.selfPeerId.collectAsState(initial = "") Row( modifier = @@ -395,3 +408,34 @@ fun PeerList( } } } + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun PromptWriteStoragePermissionsIfNecessary() { + val writeStoragePermissionState = + rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) + + val showDialog = remember { MutableStateFlow(false) } + + val requestPermissionLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> + if (!granted) { + showDialog.value = true + } + } + + LaunchedEffect(writeStoragePermissionState) { + if (!writeStoragePermissionState.status.isGranted && + writeStoragePermissionState.status.shouldShowRationale) { + showDialog.value = true + } else { + requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + + if (showDialog.collectAsState().value) { + ErrorDialog(title = R.string.permission_required, message = R.string.taildrop_requires_write) { + showDialog.value = false + } + } +} diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 01f4794..f8c8173 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -160,4 +160,7 @@ Shows or hides the UI to run the Android device as an exit node. Run As Exit Node visibility + + Permission Required + Please grant access to write to external storage to be able to receive files with Taildrop.