diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af0b355..03e819a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,7 +17,6 @@ android:exported="true" > - @@ -28,16 +27,17 @@ android:theme="@style/Theme.PartyUP.NoActionBar"> - - - - + + + + + @@ -47,7 +47,6 @@ android:label="@string/app_name" > - diff --git a/app/src/main/java/me/ocv/partyup/XferActivity.java b/app/src/main/java/me/ocv/partyup/XferActivity.java index 0c390ef..1a1f7cf 100644 --- a/app/src/main/java/me/ocv/partyup/XferActivity.java +++ b/app/src/main/java/me/ocv/partyup/XferActivity.java @@ -1,5 +1,8 @@ package me.ocv.partyup; +import static java.lang.String.format; + +import android.annotation.SuppressLint; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -14,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity; import android.provider.OpenableColumns; +import android.util.Log; import android.view.Gravity; import android.view.View; import android.widget.Button; @@ -31,67 +35,84 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; +import java.util.ArrayList; import java.util.Base64; -import java.util.stream.Collectors; import me.ocv.partyup.databinding.ActivityXferBinding; -public class XferActivity extends AppCompatActivity { +class F { + public Uri handle; + public String name; + public long size; + public String full_url; + public String share_url; + public String desc; +} +public class XferActivity extends AppCompatActivity { ActivityXferBinding binding; SharedPreferences prefs; Intent the_intent; String password; - String the_msg; - String base_url, full_url, share_url; - Uri src_uri; - String src_name; - long src_size; - String the_desc; + String base_url; boolean upping; + String the_msg; + long bytes_done, bytes_total; + F[] files; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + upping = false; + prefs = PreferenceManager.getDefaultSharedPreferences(this); binding = ActivityXferBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbar); - prefs = PreferenceManager.getDefaultSharedPreferences(this); - the_intent = getIntent(); + String etype = the_intent.getType(); String action = the_intent.getAction(); - String type = the_intent.getType(); - - if (!the_intent.ACTION_SEND.equals(action) || type == null) { - show_msg("cannot share this content"); + boolean one = Intent.ACTION_SEND.equals(action); + boolean many = Intent.ACTION_SEND_MULTIPLE.equals(action); + if (etype == null || (!one && !many)) { + show_msg("cannot share content;\naction: " + action + "\ntype: " + etype); return; } - upping = false; - the_msg = null; - src_name = null; - src_size = -1; - src_uri = (Uri)the_intent.getParcelableExtra(Intent.EXTRA_STREAM); - the_msg = the_intent.getStringExtra(Intent.EXTRA_TEXT); - if (src_uri != null) { + Uri[] handles = null; + if (many) { + ArrayList x = the_intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + handles = x.toArray(new Uri[0]); + } + else if (one) { + Uri uri = (Uri) the_intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (uri != null) + handles = new Uri[]{ uri }; + else + the_msg = the_intent.getStringExtra(Intent.EXTRA_TEXT); + } + if (handles != null) { + files = new F[handles.length]; + for (int a = 0; a < handles.length; a++) { + F f = new F(); + f.handle = handles[a]; + f.name = null; + f.size = -1; + files[a] = f; + } handleSendImage(); } else if (the_msg != null) { handleSendText(); } else { - show_msg("cannot decide on what to send for " + type); + show_msg("cannot decide on what to send for " + the_intent.getType()); return; } final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); - fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - fab.setVisibility(View.GONE); - do_up(); - } + fab.setOnClickListener(v -> { + fab.setVisibility(View.GONE); + do_up(); }); } @@ -101,12 +122,7 @@ private void show_msg(String txt) { private void tshow_msg(String txt) { final TextView tv = (TextView)findViewById(R.id.upper_info); - tv.post(new Runnable() { - @Override - public void run() { - tv.setText(txt); - } - }); + tv.post(() -> tv.setText(txt)); } String getext(String mime) { @@ -127,7 +143,7 @@ String getext(String mime) { if (mime.contains("/")) { mime = mime.split("/")[1]; - if (!mime.contains(".") && mime.length() < 8) + if (mime.matches("^[a-zA-Z0-9]{1,8}$")) return mime; } @@ -140,41 +156,68 @@ private void handleSendText() { do_up(); } + @SuppressLint("DefaultLocale") private void handleSendImage() { - // the following code returns the wrong filesize (off by 626 bytes) - Cursor cur = getContentResolver().query(src_uri, null, null, null, null); - int iname = cur.getColumnIndex(OpenableColumns.DISPLAY_NAME); - int isize = cur.getColumnIndex(OpenableColumns.SIZE); - cur.moveToFirst(); - src_name = cur.getString(iname); - src_size = cur.getLong(isize); - cur.close(); - - if (src_name == null) - src_name = "mystery-file." + getext(the_intent.getType()); - - // get correct filesize - try { - InputStream ins = getContentResolver().openInputStream(src_uri); - byte[] buf = new byte[128 * 1024]; - long sz = 0; - while (true) { - int n = ins.read(buf); - if (n <= 0) - break; - - sz += n; + for (F f : files) { + // contentresolver returns the wrong filesize (off by 626 bytes) + // but we want the name so lets go + try { + Cursor cur = getContentResolver().query(f.handle, null, null, null, null); + assert cur != null; + int iname = cur.getColumnIndex(OpenableColumns.DISPLAY_NAME); + int isize = cur.getColumnIndex(OpenableColumns.SIZE); + cur.moveToFirst(); + f.name = cur.getString(iname); + f.size = cur.getLong(isize); + cur.close(); + } + catch (Exception ex) { + Log.w("me.ocv.partyup", "contentresolver: " + ex.toString()); } - src_size = sz; + + if (f.name == null) + f.name = "mystery-file." + getext(the_intent.getType()); + + // get correct filesize + try { + InputStream ins = getContentResolver().openInputStream(f.handle); + assert ins != null; + byte[] buf = new byte[128 * 1024]; + long sz = 0; + while (true) { + int n = ins.read(buf); + if (n <= 0) + break; + + sz += n; + } + f.size = sz; + } catch (Exception ex) { + show_msg("Error3: " + ex.toString()); + return; + } + + f.desc = format("%s\n\nsize: %,d byte\ntype: %s", f.name, f.size, the_intent.getType()); } - catch (Exception ex) { - show_msg("Error3: " + ex.toString()); - return; + + String msg; + if (files.length == 1) { + msg = "Upload the following file?\n\n" + files[0].desc; } + else { + bytes_done = bytes_total = 0; + msg = "Upload the following " + files.length + " files?\n\n"; + for (int a = 0; a < Math.min(10, files.length); a++) { + msg += files[a].name + "\n"; + bytes_total += files[a].size; + } - the_desc = String.format("%s\n\nsize: %,d byte\ntype: %s", src_name, src_size, the_intent.getType()); + if (files.length > 10) + msg += "[...]\n"; - show_msg("Upload the following file?\n\n" + the_desc); + msg += format("--- total %,d bytes ---", bytes_total); + } + show_msg(msg); if (prefs.getBoolean("autosend", false)) do_up(); } @@ -184,49 +227,51 @@ private void do_up() { return; upping = true; + new Thread(this::do_up2).start(); + } + private void do_up2() { try { base_url = prefs.getString("server_url", ""); + if (base_url == null) + throw new Exception("server_url config is invalid"); + if (!base_url.startsWith("http")) base_url = "http://" + base_url; - full_url = base_url; - show_msg("Sending to " + base_url + " ...\n\n" + the_desc); - - if (!full_url.endsWith("/")) - full_url += "/"; - - if (src_size >= 0 && !src_name.isEmpty()) - full_url += URLEncoder.encode(src_name, "UTF-8"); + if (!base_url.endsWith("/")) + base_url += "/"; password = prefs.getString("server_password", ""); + password = password == null ? "" : password; if (password.equals("Default value")) password = ""; // necessary in the emulator, not on real devices(?) - Thread t = new Thread() { - public void run() { - do_up2(); + tshow_msg("Sending to " + base_url + " ..."); + + int nfiles = files == null ? 1 : files.length; + for (int a = 0; a < nfiles; a++) { + String full_url = base_url; + if (files != null) { + F f = files[a]; + full_url += URLEncoder.encode(f.name, "UTF-8"); + tshow_msg("Sending to " + base_url + " ...\n\n" + f.desc); + f.full_url = full_url; } - }; - t.start(); - } - catch (Exception ex) { - show_msg("Error1: " + ex.toString()); - } - } - private void do_up2() { - try { - URL url = new URL(full_url); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setDoOutput(true); - if (!password.isEmpty()) - conn.setRequestProperty("Authorization", "Basic " + new String(Base64.getEncoder().encode(password.getBytes()))); - - if (src_uri != null) - do_fileput(conn); - else - do_textmsg(conn); + URL url = new URL(full_url); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setDoOutput(true); + if (!password.isEmpty()) + conn.setRequestProperty("Authorization", "Basic " + new String(Base64.getEncoder().encode(password.getBytes()))); + + if (files == null) + do_textmsg(conn); + else + if (!do_fileput(conn, a)) + return; + } + findViewById(R.id.upper_info).post(() -> onsuccess()); } catch (Exception ex) { tshow_msg("Error2: " + ex.toString() + "\n\nmaybe wrong password?"); @@ -237,7 +282,7 @@ String read_err(HttpURLConnection conn) { try { byte[] buf = new byte[1024]; int n = Math.max(0, conn.getErrorStream().read(buf)); - return new String(buf, 0, n, "UTF-8"); + return new String(buf, 0, n, StandardCharsets.UTF_8); } catch (Exception ex) { return ex.toString(); } @@ -253,32 +298,27 @@ private void do_textmsg(HttpURLConnection conn) throws Exception { os.write(body); os.flush(); int rc = conn.getResponseCode(); - share_url = "HTTP " + rc; if (rc >= 300) { tshow_msg("Server error " + rc + ":\n" + read_err(conn)); conn.disconnect(); return; } conn.disconnect(); - findViewById(R.id.upper_info).post(new Runnable() { - @Override - public void run() { - onsuccess(false); - } - }); } - private void do_fileput(HttpURLConnection conn) throws Exception { + @SuppressLint("DefaultLocale") + private boolean do_fileput(HttpURLConnection conn, int nfile) throws Exception { + F f = files[nfile]; conn.setRequestMethod("PUT"); - conn.setFixedLengthStreamingMode(src_size); + conn.setFixedLengthStreamingMode(f.size); conn.setRequestProperty("Content-Type", "application/octet-stream"); conn.connect(); final TextView tv = (TextView) findViewById(R.id.upper_info); OutputStream os = conn.getOutputStream(); - InputStream ins = getContentResolver().openInputStream(src_uri); + InputStream ins = getContentResolver().openInputStream(f.handle); MessageDigest md = MessageDigest.getInstance("SHA-512"); byte[] buf = new byte[128 * 1024]; - long bytes_done = 0; + assert ins != null; while (true) { int n = ins.read(buf); if (n <= 0) @@ -288,20 +328,18 @@ private void do_fileput(HttpURLConnection conn) throws Exception { os.write(buf, 0, n); md.update(buf, 0, n); - final long meme = bytes_done; - tv.post(new Runnable() { - @Override - public void run() { - double perc = ((double) meme * 100) / src_size; - tv.setText(String.format("Sending to %s ...\n\n%s\n\nbytes done: %,d\nbytes left: %,d\nprogress: %.2f %%", - base_url, - the_desc, - meme, - src_size - meme, - perc - )); - ((ProgressBar) findViewById(R.id.progbar)).setProgress((int) Math.round(perc)); - } + tv.post(() -> { + double perc = ((double) bytes_done * 100) / bytes_total; + tv.setText(format("Sending %d of %d to %s ...\n\n%s\n\nbytes done: %,d\nbytes left: %,d\nprogress: %.2f %%", + nfile + 1, + files.length, + base_url, + f.desc, + bytes_done, + bytes_total - bytes_done, + perc + )); + ((ProgressBar) findViewById(R.id.progbar)).setProgress((int) Math.round(perc)); }); } os.flush(); @@ -309,44 +347,46 @@ public void run() { if (rc >= 300) { tshow_msg("Server error " + rc + ":\n" + read_err(conn)); conn.disconnect(); - return; + return false; } String sha = ""; byte[] bsha = md.digest(); for (int a = 0; a < 28; a++) - sha += String.format("%02x", bsha[a]); + sha += format("%02x", bsha[a]); BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); - String[] lines = br.lines().collect(Collectors.toList()).toArray(new String[0]); + String[] lines = br.lines().toArray(String[]::new); conn.disconnect(); if (lines.length < 3) { tshow_msg("SERVER ERROR:\n" + lines[0]); - return; + return false; } if (lines[2].indexOf(sha) != 0) { tshow_msg("ERROR:\nFile got corrupted during the upload;\n\n" + lines[2] + " expected\n" + sha + " from server"); - return; + return false; } if (lines.length > 3 && !lines[3].isEmpty()) - share_url = lines[3]; + f.share_url = lines[3]; else - share_url = full_url.split("\\?")[0]; + f.share_url = f.full_url.split("\\?")[0]; - tv.post(new Runnable() { - @Override - public void run() { - onsuccess(true); - } - }); + return true; } - void onsuccess(Boolean upload) { - show_msg("✅ 👍\n\nCompleted successfully\n\n" + share_url); + void onsuccess() { + String msg = "✅ 👍\n\nCompleted successfully"; + if (files != null) { + if (files.length == 1) + msg += "\n\n" + files[0].share_url; + else + msg += "\n\n" + files.length + " files OK"; + } + show_msg(msg); ((TextView)findViewById(R.id.upper_info)).setGravity(Gravity.CENTER); String act = prefs.getString("on_up_ok", "menu"); - if (!act.equals("menu")) { + if (act != null && !act.equals("menu")) { if (act.equals("copy")) copylink(); else if (act.equals("share")) @@ -362,52 +402,49 @@ else if (act.equals("share")) findViewById(R.id.successbuttons).setVisibility(View.VISIBLE); Button btn = (Button)findViewById(R.id.btnExit); - btn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - finishAndRemoveTask(); - } - }); + btn.setOnClickListener(v -> finishAndRemoveTask()); - if (!upload) { - findViewById(R.id.btnCopyLink).setVisibility(View.GONE); - findViewById(R.id.btnShareLink).setVisibility(View.GONE); + Button vcopy = (Button)findViewById(R.id.btnCopyLink); + Button vshare = (Button)findViewById(R.id.btnShareLink); + if (files == null) { + vcopy.setVisibility(View.GONE); + vshare.setVisibility(View.GONE); return; } - - btn = (Button)findViewById(R.id.btnCopyLink); - btn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - copylink(); - } - }); - - btn = (Button)findViewById(R.id.btnShareLink); - btn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - sharelink(); - } - }); + vcopy.setOnClickListener(v -> copylink()); + vshare.setOnClickListener(v -> sharelink()); + if (files.length > 1) + vshare.setVisibility(View.GONE); } void copylink() { + if (files == null) + return; + + String links = ""; + for (F file : files) + links += file.share_url + "\n"; + ClipboardManager cb = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData cd = ClipData.newPlainText("copyparty upload", share_url); + ClipData cd = ClipData.newPlainText("copyparty upload", links); + cb.setPrimaryClip(cd); Toast.makeText(getApplicationContext(), "Upload OK -- Link copied", Toast.LENGTH_SHORT).show(); } void sharelink() { + if (files == null || files.length > 1) + return; + + F f = files[0]; Intent send = new Intent(Intent.ACTION_SEND); send.setType("text/plain"); send.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); send.putExtra(Intent.EXTRA_SUBJECT, "Uploaded file"); - send.putExtra(Intent.EXTRA_TEXT, share_url); + send.putExtra(Intent.EXTRA_TEXT, f.share_url); //startActivity(Intent.createChooser(send, "Share file link")); Intent view = new Intent(Intent.ACTION_VIEW); - view.setData(Uri.parse(share_url)); + view.setData(Uri.parse(f.share_url)); Intent i = Intent.createChooser(send, "Share file link"); i.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { view }); diff --git a/metadata/en-US/changelogs/10600.txt b/metadata/en-US/changelogs/10600.txt index 0041b61..a9670ab 100644 --- a/metadata/en-US/changelogs/10600.txt +++ b/metadata/en-US/changelogs/10600.txt @@ -1,2 +1,4 @@ +• new: upload multiple files • new: choose what happens when an upload succeeds • new: add http:// to the server URL if missing +• bugfix: sharing from some apps in android 10+ could crash this app \ No newline at end of file