android,cmd/tailscale: implement taildrop receive for Android < 10

Fixes tailscale/tailscale#2720
Fixes tailscale/tailscale#2296

Signed-off-by: Elias Naur <mail@eliasnaur.com>
pull/20/head
Elias Naur 3 years ago
parent f37cf72d81
commit 84b484a954

@ -3,6 +3,7 @@
package="com.tailscale.ipn">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<!-- Disable input emulation on ChromeOS -->
<uses-feature android:name="android.hardware.type.pc" android:required="false"/>

@ -6,6 +6,7 @@ package com.tailscale.ipn;
import android.app.Application;
import android.app.Activity;
import android.app.DownloadManager;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.NotificationChannel;
@ -31,6 +32,7 @@ import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.Manifest;
import android.webkit.MimeTypeMap;
import java.io.IOException;
@ -42,6 +44,8 @@ import java.security.GeneralSecurityException;
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;
@ -234,15 +238,33 @@ public class App extends Application {
return null;
}
void requestWriteStoragePermission(Activity act) {
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(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, IPNActivity.WRITE_STORAGE_RESULT);
}
String insertMedia(String name, String mimeType) throws IOException {
ContentResolver resolver = getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
if (!"".equals(mimeType)) {
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
if (!"".equals(mimeType)) {
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
}
Uri root = MediaStore.Files.getContentUri("external");
return resolver.insert(root, contentValues).toString();
} else {
File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
dir.mkdirs();
File f = new File(dir, name);
return Uri.fromFile(f).toString();
}
Uri root = MediaStore.Files.getContentUri("external");
return resolver.insert(root, contentValues).toString();
}
int openUri(String uri, String mode) throws IOException {
@ -256,8 +278,14 @@ public class App extends Application {
}
public void notifyFile(String uri, String msg) {
Intent fileIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
PendingIntent pending = PendingIntent.getActivity(this, 0, fileIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Intent viewIntent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
} else {
// uri is a file:// which is not allowed to be shared outside the app.
viewIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
}
PendingIntent pending = PendingIntent.getActivity(this, 0, viewIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, FILE_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("File received")
@ -283,4 +311,5 @@ public class App extends Application {
static native void onVPNPrepared();
private static native void onConnectivityChanged(boolean connected);
static native void onShareIntent(int nfiles, int[] types, String[] mimes, String[] items, String[] names, long[] sizes);
static native void onWriteStorageGranted();
}

@ -12,6 +12,7 @@ import android.database.Cursor;
import android.os.Bundle;
import android.provider.OpenableColumns;
import android.net.Uri;
import android.content.pm.PackageManager;
import java.util.List;
import java.util.ArrayList;
@ -19,6 +20,8 @@ import java.util.ArrayList;
import org.gioui.GioView;
public final class IPNActivity extends Activity {
final static int WRITE_STORAGE_RESULT = 1000;
private GioView view;
@Override public void onCreate(Bundle state) {
@ -89,6 +92,15 @@ public final class IPNActivity extends Activity {
App.onShareIntent(nfiles, types, mimes, items, names, sizes);
}
@Override public void onRequestPermissionsResult(int reqCode, String[] perms, int[] grants) {
switch (reqCode) {
case WRITE_STORAGE_RESULT:
if (grants.length > 0 && grants[0] == PackageManager.PERMISSION_GRANTED) {
App.onWriteStorageGranted();
}
}
}
@Override public void onDestroy() {
view.destroy();
super.onDestroy();

@ -39,6 +39,9 @@ var (
// onFileShare receives file sharing intents.
onFileShare = make(chan []File, 1)
// onWriteStorageGranted is notified when we are granted WRITE_STORAGE_PERMISSION.
onWriteStorageGranted = make(chan struct{}, 1)
)
const (
@ -58,6 +61,14 @@ func Java_com_tailscale_ipn_App_onVPNPrepared(env *C.JNIEnv, class C.jclass) {
notifyVPNPrepared()
}
//export Java_com_tailscale_ipn_App_onWriteStorageGranted
func Java_com_tailscale_ipn_App_onWriteStorageGranted(env *C.JNIEnv, class C.jclass) {
select {
case onWriteStorageGranted <- struct{}{}:
default:
}
}
func notifyVPNPrepared() {
select {
case onVPNPrepared <- struct{}{}:

@ -385,6 +385,8 @@ func (a *App) runBackend() error {
a.targetsLoaded <- FileTargets{targets, err}
waitingFiles = n.FilesWaiting != nil
processFiles()
case <-onWriteStorageGranted:
processFiles()
case <-alarmChan:
if m := state.NetworkMap; m != nil && service != 0 {
alarm(a.notifyExpiry(service, m.Expiry))
@ -864,6 +866,11 @@ func (a *App) runUI() error {
if err := a.callVoidMethod(a.appCtx, "startVPN", "()V"); err != nil {
return err
}
if activity != 0 {
if err := a.callVoidMethod(a.appCtx, "requestWriteStoragePermission", "(Landroid/app/Activity;)V", jni.Value(activity)); err != nil {
return err
}
}
}
case <-onVPNRevoked:
ui.ShowMessage("VPN access denied or another VPN service is always-on")

Loading…
Cancel
Save