Skip to content

Commit 10ded1b

Browse files
committed
cmd/tailscale,java: implement file sharing
Fixes tailscale/tailscale#1809 Signed-off-by: Elias Naur <mail@eliasnaur.com>
1 parent 331bc1e commit 10ded1b

9 files changed

Lines changed: 811 additions & 63 deletions

File tree

android/src/main/AndroidManifest.xml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
<application android:label="Tailscale" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round"
1111
android:name=".App" android:allowBackup="false">
12-
<activity android:name="org.gioui.GioActivity"
12+
<activity android:name="IPNActivity"
1313
android:label="@string/app_name"
1414
android:theme="@style/Theme.GioApp"
1515
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden"
@@ -22,6 +22,28 @@
2222
<intent-filter>
2323
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
2424
</intent-filter>
25+
<intent-filter>
26+
<action android:name="android.intent.action.SEND" />
27+
<category android:name="android.intent.category.DEFAULT"/>
28+
<data android:mimeType="application/*" />
29+
<data android:mimeType="audio/*" />
30+
<data android:mimeType="image/*" />
31+
<data android:mimeType="message/*" />
32+
<data android:mimeType="multipart/*" />
33+
<data android:mimeType="text/*" />
34+
<data android:mimeType="video/*" />
35+
</intent-filter>
36+
<intent-filter>
37+
<action android:name="android.intent.action.SEND_MULTIPLE" />
38+
<category android:name="android.intent.category.DEFAULT" />
39+
<data android:mimeType="application/*" />
40+
<data android:mimeType="audio/*" />
41+
<data android:mimeType="image/*" />
42+
<data android:mimeType="message/*" />
43+
<data android:mimeType="multipart/*" />
44+
<data android:mimeType="text/*" />
45+
<data android:mimeType="video/*" />
46+
</intent-filter>
2547
</activity>
2648
<service android:name=".IPNService"
2749
android:permission="android.permission.BIND_VPN_SERVICE">

android/src/main/java/com/tailscale/ipn/App.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,40 @@
88
import android.app.Activity;
99
import android.app.Fragment;
1010
import android.app.FragmentTransaction;
11+
import android.app.NotificationChannel;
12+
import android.app.PendingIntent;
1113
import android.content.BroadcastReceiver;
14+
import android.content.ContentResolver;
15+
import android.content.ContentValues;
1216
import android.content.Context;
1317
import android.content.Intent;
1418
import android.content.IntentFilter;
1519
import android.content.SharedPreferences;
1620
import android.content.pm.PackageManager;
1721
import android.content.pm.PackageInfo;
1822
import android.content.pm.Signature;
23+
import android.provider.MediaStore;
1924
import android.provider.Settings;
2025
import android.net.ConnectivityManager;
2126
import android.net.Uri;
2227
import android.net.VpnService;
2328
import android.view.View;
2429
import android.os.Build;
30+
import android.os.Environment;
2531
import android.os.Handler;
2632
import android.os.Looper;
2733

34+
import android.webkit.MimeTypeMap;
35+
2836
import java.io.IOException;
2937
import java.io.File;
3038
import java.io.FileOutputStream;
3139

3240
import java.security.GeneralSecurityException;
3341

42+
import androidx.core.app.NotificationCompat;
43+
import androidx.core.app.NotificationManagerCompat;
44+
3445
import androidx.security.crypto.EncryptedSharedPreferences;
3546
import androidx.security.crypto.MasterKey;
3647

@@ -41,13 +52,27 @@
4152
public class App extends Application {
4253
private final static String PEER_TAG = "peer";
4354

55+
static final String STATUS_CHANNEL_ID = "tailscale-status";
56+
static final int STATUS_NOTIFICATION_ID = 1;
57+
58+
static final String NOTIFY_CHANNEL_ID = "tailscale-notify";
59+
static final int NOTIFY_NOTIFICATION_ID = 2;
60+
61+
private static final String FILE_CHANNEL_ID = "tailscale-files";
62+
private static final int FILE_NOTIFICATION_ID = 3;
63+
4464
private final static Handler mainHandler = new Handler(Looper.getMainLooper());
4565

4666
@Override public void onCreate() {
4767
super.onCreate();
4868
// Load and initialize the Go library.
4969
Gio.init(this);
5070
registerNetworkCallback();
71+
72+
createNotificationChannel(NOTIFY_CHANNEL_ID, "Notifications", NotificationManagerCompat.IMPORTANCE_DEFAULT);
73+
createNotificationChannel(STATUS_CHANNEL_ID, "VPN Status", NotificationManagerCompat.IMPORTANCE_LOW);
74+
createNotificationChannel(FILE_CHANNEL_ID, "File transfers", NotificationManagerCompat.IMPORTANCE_DEFAULT);
75+
5176
}
5277

5378
private void registerNetworkCallback() {
@@ -209,6 +234,53 @@ byte[] getPackageCertificate() throws Exception {
209234
return null;
210235
}
211236

237+
String insertMedia(String name, String mimeType) throws IOException {
238+
ContentResolver resolver = getContentResolver();
239+
ContentValues contentValues = new ContentValues();
240+
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
241+
if (!"".equals(mimeType)) {
242+
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
243+
}
244+
Uri root = MediaStore.Files.getContentUri("external");
245+
return resolver.insert(root, contentValues).toString();
246+
}
247+
248+
int openUri(String uri, String mode) throws IOException {
249+
ContentResolver resolver = getContentResolver();
250+
return resolver.openFileDescriptor(Uri.parse(uri), mode).detachFd();
251+
}
252+
253+
void deleteUri(String uri) {
254+
ContentResolver resolver = getContentResolver();
255+
resolver.delete(Uri.parse(uri), null, null);
256+
}
257+
258+
public void notifyFile(String uri, String msg) {
259+
Intent fileIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
260+
PendingIntent pending = PendingIntent.getActivity(this, 0, fileIntent, PendingIntent.FLAG_UPDATE_CURRENT);
261+
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, FILE_CHANNEL_ID)
262+
.setSmallIcon(R.drawable.ic_notification)
263+
.setContentTitle("File received")
264+
.setContentText(msg)
265+
.setContentIntent(pending)
266+
.setAutoCancel(true)
267+
.setOnlyAlertOnce(true)
268+
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
269+
270+
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
271+
nm.notify(FILE_NOTIFICATION_ID, builder.build());
272+
}
273+
274+
private void createNotificationChannel(String id, String name, int importance) {
275+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
276+
return;
277+
}
278+
NotificationChannel channel = new NotificationChannel(id, name, importance);
279+
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
280+
nm.createNotificationChannel(channel);
281+
}
282+
212283
static native void onVPNPrepared();
213284
private static native void onConnectivityChanged(boolean connected);
285+
static native void onShareIntent(int nfiles, int[] types, String[] mimes, String[] items, String[] names, long[] sizes);
214286
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package com.tailscale.ipn;
6+
7+
import android.app.Activity;
8+
import android.content.res.AssetFileDescriptor;
9+
import android.content.res.Configuration;
10+
import android.content.Intent;
11+
import android.database.Cursor;
12+
import android.os.Bundle;
13+
import android.provider.OpenableColumns;
14+
import android.net.Uri;
15+
16+
import java.util.List;
17+
import java.util.ArrayList;
18+
19+
import org.gioui.GioView;
20+
21+
public final class IPNActivity extends Activity {
22+
private GioView view;
23+
24+
@Override public void onCreate(Bundle state) {
25+
super.onCreate(state);
26+
view = new GioView(this);
27+
setContentView(view);
28+
handleIntent();
29+
}
30+
31+
@Override public void onNewIntent(Intent i) {
32+
setIntent(i);
33+
handleIntent();
34+
}
35+
36+
private void handleIntent() {
37+
Intent it = getIntent();
38+
String act = it.getAction();
39+
String[] texts;
40+
Uri[] uris;
41+
if (Intent.ACTION_SEND.equals(act)) {
42+
uris = new Uri[]{it.getParcelableExtra(Intent.EXTRA_STREAM)};
43+
texts = new String[]{it.getStringExtra(Intent.EXTRA_TEXT)};
44+
} else if (Intent.ACTION_SEND_MULTIPLE.equals(act)) {
45+
List<Uri> extraUris = it.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
46+
uris = extraUris.toArray(new Uri[0]);
47+
texts = new String[uris.length];
48+
} else {
49+
return;
50+
}
51+
String mime = it.getType();
52+
int nitems = uris.length;
53+
String[] items = new String[nitems];
54+
String[] mimes = new String[nitems];
55+
int[] types = new int[nitems];
56+
String[] names = new String[nitems];
57+
long[] sizes = new long[nitems];
58+
int nfiles = 0;
59+
for (int i = 0; i < uris.length; i++) {
60+
String text = texts[i];
61+
Uri uri = uris[i];
62+
if (text != null) {
63+
types[nfiles] = 1; // FileTypeText
64+
names[nfiles] = "file.txt";
65+
mimes[nfiles] = mime;
66+
items[nfiles] = text;
67+
// Determined by len(text) in Go to eliminate UTF-8 encoding differences.
68+
sizes[nfiles] = 0;
69+
nfiles++;
70+
} else if (uri != null) {
71+
Cursor c = getContentResolver().query(uri, null, null, null, null);
72+
int nameCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
73+
int sizeCol = c.getColumnIndex(OpenableColumns.SIZE);
74+
c.moveToFirst();
75+
String name = c.getString(nameCol);
76+
long size = c.getLong(sizeCol);
77+
types[nfiles] = 2; // FileTypeURI
78+
mimes[nfiles] = mime;
79+
items[nfiles] = uri.toString();
80+
names[nfiles] = name;
81+
sizes[nfiles] = size;
82+
nfiles++;
83+
}
84+
}
85+
App.onShareIntent(nfiles, types, mimes, items, names, sizes);
86+
}
87+
88+
@Override public void onDestroy() {
89+
view.destroy();
90+
super.onDestroy();
91+
}
92+
93+
@Override public void onStart() {
94+
super.onStart();
95+
view.start();
96+
}
97+
98+
@Override public void onStop() {
99+
view.stop();
100+
super.onStop();
101+
}
102+
103+
@Override public void onConfigurationChanged(Configuration c) {
104+
super.onConfigurationChanged(c);
105+
view.configurationChanged();
106+
}
107+
108+
@Override public void onLowMemory() {
109+
super.onLowMemory();
110+
view.onLowMemory();
111+
}
112+
113+
@Override public void onBackPressed() {
114+
if (!view.backPressed())
115+
super.onBackPressed();
116+
}
117+
}

android/src/main/java/com/tailscale/ipn/IPNService.java

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import android.os.Build;
88
import android.app.PendingIntent;
9-
import android.app.NotificationChannel;
109
import android.content.Intent;
1110
import android.content.pm.PackageManager;
1211
import android.net.VpnService;
@@ -21,14 +20,6 @@ public class IPNService extends VpnService {
2120
public static final String ACTION_CONNECT = "com.tailscale.ipn.CONNECT";
2221
public static final String ACTION_DISCONNECT = "com.tailscale.ipn.DISCONNECT";
2322

24-
private static final String STATUS_CHANNEL_ID = "tailscale-status";
25-
private static final String STATUS_CHANNEL_NAME = "VPN Status";
26-
private static final int STATUS_NOTIFICATION_ID = 1;
27-
28-
private static final String NOTIFY_CHANNEL_ID = "tailscale-notify";
29-
private static final String NOTIFY_CHANNEL_NAME = "Notifications";
30-
private static final int NOTIFY_NOTIFICATION_ID = 2;
31-
3223
@Override public int onStartCommand(Intent intent, int flags, int startId) {
3324
if (intent != null && ACTION_DISCONNECT.equals(intent.getAction())) {
3425
close();
@@ -70,9 +61,7 @@ protected VpnService.Builder newBuilder() {
7061
}
7162

7263
public void notify(String title, String message) {
73-
createNotificationChannel(NOTIFY_CHANNEL_ID, NOTIFY_CHANNEL_NAME, NotificationManagerCompat.IMPORTANCE_DEFAULT);
74-
75-
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFY_CHANNEL_ID)
64+
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.NOTIFY_CHANNEL_ID)
7665
.setSmallIcon(R.drawable.ic_notification)
7766
.setContentTitle(title)
7867
.setContentText(message)
@@ -82,29 +71,18 @@ public void notify(String title, String message) {
8271
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
8372

8473
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
85-
nm.notify(NOTIFY_NOTIFICATION_ID, builder.build());
74+
nm.notify(App.NOTIFY_NOTIFICATION_ID, builder.build());
8675
}
8776

8877
public void updateStatusNotification(String title, String message) {
89-
createNotificationChannel(STATUS_CHANNEL_ID, STATUS_CHANNEL_NAME, NotificationManagerCompat.IMPORTANCE_LOW);
90-
91-
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, STATUS_CHANNEL_ID)
78+
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, App.STATUS_CHANNEL_ID)
9279
.setSmallIcon(R.drawable.ic_notification)
9380
.setContentTitle(title)
9481
.setContentText(message)
9582
.setContentIntent(configIntent())
9683
.setPriority(NotificationCompat.PRIORITY_LOW);
9784

98-
startForeground(STATUS_NOTIFICATION_ID, builder.build());
99-
}
100-
101-
private void createNotificationChannel(String id, String name, int importance) {
102-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
103-
return;
104-
}
105-
NotificationChannel channel = new NotificationChannel(id, name, importance);
106-
NotificationManagerCompat nm = NotificationManagerCompat.from(this);
107-
nm.createNotificationChannel(channel);
85+
startForeground(App.STATUS_NOTIFICATION_ID, builder.build());
10886
}
10987

11088
private native void connect();

0 commit comments

Comments
 (0)