├── .gitignore ├── LICENSE ├── PRIVACY_POLICY.md ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── slowscript │ │ └── warpinator │ │ ├── AboutActivity.java │ │ ├── Authenticator.java │ │ ├── Autostart.java │ │ ├── CertServer.java │ │ ├── GrpcService.java │ │ ├── LocalBroadcasts.java │ │ ├── MainActivity.java │ │ ├── MainService.java │ │ ├── MainServiceBinder.java │ │ ├── Remote.java │ │ ├── RemotesAdapter.java │ │ ├── Server.java │ │ ├── SettingsActivity.java │ │ ├── ShareActivity.java │ │ ├── TileMainService.java │ │ ├── Transfer.java │ │ ├── TransfersActivity.java │ │ ├── TransfersAdapter.java │ │ ├── Utils.java │ │ ├── WarpinatorApp.java │ │ ├── ZlibCompressor.java │ │ └── preferences │ │ ├── EditTextPreference.java │ │ ├── ListPreference.java │ │ ├── ProfilePicturePreference.java │ │ └── ResetablePreference.java │ ├── proto │ └── warp.proto │ └── res │ ├── anim │ ├── anim_null.xml │ ├── anim_push_down.xml │ └── anim_push_up.xml │ ├── drawable-hdpi │ └── ic_notification.png │ ├── drawable-mdpi │ └── ic_notification.png │ ├── drawable-night │ └── transfers_background.xml │ ├── drawable-v31 │ └── ic_launcher_foreground.xml │ ├── drawable-xhdpi │ └── ic_notification.png │ ├── drawable-xxhdpi │ └── ic_notification.png │ ├── drawable-xxxhdpi │ └── ic_notification.png │ ├── drawable │ ├── ic_accept.xml │ ├── ic_decline.xml │ ├── ic_download.xml │ ├── ic_error.xml │ ├── ic_file.xml │ ├── ic_folder.xml │ ├── ic_launcher_foreground.xml │ ├── ic_meh.xml │ ├── ic_reconnect.xml │ ├── ic_remove_items.xml │ ├── ic_star_empty.xml │ ├── ic_star_full.xml │ ├── ic_star_toggle.xml │ ├── ic_status_awaiting_duplex.xml │ ├── ic_status_connected.xml │ ├── ic_status_connecting.xml │ ├── ic_stop.xml │ ├── ic_unavailable.xml │ ├── ic_upload.xml │ ├── ic_user.xml │ ├── ic_warpinator.xml │ ├── rounded_corners.xml │ └── transfers_background.xml │ ├── layout │ ├── activity_about.xml │ ├── activity_main.xml │ ├── activity_share.xml │ ├── activity_transfers.xml │ ├── dialog_manual_connect.xml │ ├── profile_chooser_view.xml │ ├── remote_view.xml │ ├── resetable_preference.xml │ ├── settings_activity.xml │ ├── simple_list_item.xml │ └── transfer_view.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-ca │ └── strings.xml │ ├── values-cs │ └── strings.xml │ ├── values-de │ └── strings.xml │ ├── values-es │ └── strings.xml │ ├── values-fa │ └── strings.xml │ ├── values-fr │ └── strings.xml │ ├── values-hi │ └── strings.xml │ ├── values-hr │ └── strings.xml │ ├── values-hu │ └── strings.xml │ ├── values-id │ └── strings.xml │ ├── values-it │ └── strings.xml │ ├── values-kab │ └── strings.xml │ ├── values-lv │ └── strings.xml │ ├── values-ml │ └── strings.xml │ ├── values-nb-rNO │ └── strings.xml │ ├── values-nl │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ro │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-sl │ └── strings.xml │ ├── values-sv │ └── strings.xml │ ├── values-ta │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-v31 │ └── ic_launcher_background.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── arrays.xml │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ ├── provider_paths.xml │ └── root_preferences.xml ├── build.gradle ├── connection-issues-ptbr.md ├── connection-issues.md ├── fastlane └── metadata │ └── android │ ├── ca-ES │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── cs-CZ │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── de-DE │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── en-US │ ├── changelogs │ │ ├── 1050.txt │ │ ├── 1051.txt │ │ ├── 1052.txt │ │ ├── 1053.txt │ │ ├── 1060.txt │ │ ├── 1061.txt │ │ ├── 1070.txt │ │ ├── 1071.txt │ │ ├── 1080.txt │ │ ├── 1081.txt │ │ └── 1082.txt │ ├── full_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.jpg │ │ │ └── 2.jpg │ ├── short_description.txt │ └── title.txt │ ├── es-ES │ ├── full_description.txt │ ├── images │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ ├── short_description.txt │ └── title.txt │ ├── fr-FR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── hu-HU │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── nl-NL │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── pl-PL │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── pt-BR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ ├── ta-IN │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt │ └── tr-TR │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/* 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | app/release/* 11 | 12 | -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | # Privacy policy 2 | 3 | Warpinator for Android is a free and open source project that respects your privacy. As such, it collects no personal data. 4 | That being said, you may still want to know which information the application has access to and how it is handled. 5 | 6 | To allow device discovery, we use the mDNS protocol. It works by sending the information that a Warpinator service is running on a certain IP address 7 | and port to everyone on the local network. It also discloses the phone's name to allow other users identify the device. 8 | 9 | The devices then make an encrypted connection. All subsequent communication (transfers, transfer requests, profile picture and 10 | username, ...) goes through this channel. Only users with the same group code are able to connect. 11 | No personal files are accessed unless instructed by the user to be tranferred to a selected device. 12 | 13 | The application can also log debug information if enabled in the settings. The log file is kept in a location that can be accessed by some other apps 14 | with sufficient permissions (eg. file managers, but also others). As the log may contain sensitive information (transferred file names, 15 | connected devices, ...), do not keep this option enabled unless needed for troubleshooting. 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Warpinator for Android (unofficial) 2 | 3 | This is an unofficial reimplementation of Linux Mint's file sharing tool [Warpinator](https://github.com/linuxmint/warpinator) for Android. 4 | 5 | ## Download 6 | Get the APK from the [Releases](https://github.com/slowscript/warpinator-android/releases) page 7 | 8 | Also available on F-Droid and Google Play 9 | 10 | Get it on Google Play 11 | 12 | ## Building 13 | 14 | Build with Android Studio or with this command (you will need to install Android SDK yourself though): 15 | 16 | ``` 17 | export ANDROID_SDK_ROOT=$HOME/Android/Sdk 18 | ./gradlew :app:assembleDebug 19 | ``` 20 | 21 | ## Translations 22 | 23 | Translations are now being done through [Weblate](https://hosted.weblate.org/projects/warpinator-android/) 24 | 25 | [![Weblate](https://hosted.weblate.org/widgets/warpinator-android/-/app/88x31-white.png)](https://hosted.weblate.org/engage/warpinator-android/) 26 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'com.google.protobuf' 3 | 4 | android { 5 | compileSdk 34 6 | 7 | defaultConfig { 8 | applicationId 'slowscript.warpinator' 9 | minSdkVersion 21 //Required by NSD (attributes) 10 | targetSdkVersion 34 11 | versionCode 1082 12 | versionName "1.8.2" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled true 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility = 17 25 | targetCompatibility = 17 26 | } 27 | packagingOptions { 28 | resources { 29 | excludes += ['META-INF/INDEX.LIST', 'META-INF/io.netty.versions.properties'] 30 | } 31 | } 32 | namespace 'slowscript.warpinator' 33 | } 34 | 35 | dependencies { 36 | implementation fileTree(dir: 'libs', include: ['*.jar']) 37 | 38 | implementation 'androidx.appcompat:appcompat:1.7.0' 39 | implementation 'androidx.constraintlayout:constraintlayout:2.2.1' 40 | implementation 'androidx.documentfile:documentfile:1.0.1' 41 | implementation 'com.google.android.material:material:1.12.0' 42 | implementation 'androidx.recyclerview:recyclerview:1.3.2' //1.4 needs SDK 35 43 | implementation 'androidx.cardview:cardview:1.0.0' 44 | 45 | implementation 'org.openjax.security:nacl:0.3.2' //Update available, but API is weird now 46 | implementation 'org.bouncycastle:bcpkix-jdk14:1.80' 47 | implementation 'io.grpc:grpc-netty:1.49.0' //Updating gRPC caused a problem with connecting, must investigate 48 | implementation 'io.grpc:grpc-okhttp:1.49.0' 49 | implementation ('io.grpc:grpc-protobuf:1.49.0') { 50 | exclude group: 'com.google.api.grpc', module: 'proto-google-common-protos' 51 | } 52 | implementation 'io.grpc:grpc-stub:1.49.0' 53 | implementation 'javax.annotation:javax.annotation-api:1.3.2' 54 | implementation 'org.conscrypt:conscrypt-android:2.5.3' 55 | 56 | implementation 'com.github.tony19:logback-android:3.0.0' 57 | implementation 'androidx.preference:preference:1.2.1' 58 | implementation 'com.google.guava:guava:33.4.0-android' //This was included by gRPC anyway, so why not use it 59 | implementation 'org.jmdns:jmdns:3.5.8' //Device discovery worsened after update, let's see if this was the problem 60 | // Also, new versions require desugaring on Android 5 and 6 61 | implementation 'org.slf4j:slf4j-api:2.0.17' // For jmdns, it declares a too old dependency 62 | implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' 63 | implementation 'com.google.zxing:core:3.5.3' 64 | } 65 | 66 | protobuf { 67 | protoc { 68 | artifact = 'com.google.protobuf:protoc:3.21.5' 69 | } 70 | plugins { 71 | grpc { 72 | artifact = 'io.grpc:protoc-gen-grpc-java:1.57.0' 73 | } 74 | } 75 | generateProtoTasks { 76 | all().each { task -> 77 | task.builtins { 78 | java { } 79 | } 80 | task.plugins { 81 | grpc { } 82 | } 83 | } 84 | } 85 | } 86 | 87 | //If there is a better way to get rid of Netty logging, let me know 88 | configurations.all { 89 | resolutionStrategy { 90 | dependencySubstitution { 91 | substitute module('ch.qos.logback:logback-classic') using module('com.github.tony19:logback-android:3.0.0') 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | -keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -dontobfuscate 24 | 25 | # Netty 26 | -keep public class io.netty.util.ReferenceCountUtil { 27 | *; 28 | } 29 | -keep public interface io.netty.channel.ChannelOutboundInvoker { 30 | *; 31 | } 32 | # These should not be needed 33 | -dontwarn com.android.org.conscrypt.SSLParametersImpl 34 | -dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl 35 | -dontwarn reactor.blockhound.integration.BlockHoundIntegration 36 | -dontwarn io.netty.internal.tcnative.* 37 | -dontwarn org.jetbrains.annotations.* 38 | # ?? 39 | -dontwarn org.apache.log4j.Level 40 | -dontwarn org.apache.log4j.Logger 41 | -dontwarn org.apache.log4j.Priority 42 | -dontwarn org.apache.logging.log4j.Level 43 | -dontwarn org.apache.logging.log4j.LogManager 44 | -dontwarn org.apache.logging.log4j.Logger 45 | -dontwarn org.apache.logging.log4j.message.MessageFactory 46 | -dontwarn org.apache.logging.log4j.spi.ExtendedLogger 47 | -dontwarn org.apache.logging.log4j.spi.ExtendedLoggerWrapper 48 | -dontwarn org.eclipse.jetty.alpn.ALPN$ClientProvider 49 | -dontwarn org.eclipse.jetty.alpn.ALPN$Provider 50 | -dontwarn org.eclipse.jetty.alpn.ALPN$ServerProvider 51 | -dontwarn org.eclipse.jetty.alpn.ALPN 52 | -dontwarn org.eclipse.jetty.npn.NextProtoNego$ClientProvider 53 | -dontwarn org.eclipse.jetty.npn.NextProtoNego$Provider 54 | -dontwarn org.eclipse.jetty.npn.NextProtoNego$ServerProvider 55 | -dontwarn org.eclipse.jetty.npn.NextProtoNego -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 61 | 62 | 64 | 65 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 86 | 87 | 88 | 89 | 90 | 91 | 93 | 94 | 100 | 101 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 114 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slowscript/warpinator-android/3a13e402f1f547a9737478297656c6688614d05c/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/AboutActivity.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator; 2 | 3 | import androidx.appcompat.app.ActionBar; 4 | import androidx.appcompat.app.AppCompatActivity; 5 | 6 | import android.os.Bundle; 7 | import android.text.Html; 8 | import android.text.method.LinkMovementMethod; 9 | import android.view.MenuItem; 10 | import android.widget.TextView; 11 | 12 | public class AboutActivity extends AppCompatActivity { 13 | 14 | TextView versionView, warrantyView; 15 | 16 | @Override 17 | protected void onCreate(Bundle savedInstanceState) { 18 | super.onCreate(savedInstanceState); 19 | setContentView(R.layout.activity_about); 20 | ActionBar actionBar = getSupportActionBar(); 21 | if (actionBar != null) { 22 | actionBar.setDisplayHomeAsUpEnabled(true); 23 | } 24 | 25 | versionView = findViewById(R.id.versionText); 26 | warrantyView = findViewById(R.id.warrantyText); 27 | 28 | versionView.setText(getString(R.string.version, BuildConfig.VERSION_NAME)); 29 | warrantyView.setMovementMethod(LinkMovementMethod.getInstance()); 30 | warrantyView.setText(Html.fromHtml(getResources().getString(R.string.warranty_html))); 31 | 32 | } 33 | 34 | @Override 35 | public boolean onOptionsItemSelected(MenuItem item) { 36 | // Respond to the action bar's Up/Home button 37 | if (item.getItemId() == android.R.id.home) { 38 | super.onBackPressed(); 39 | return true; 40 | } 41 | return super.onOptionsItemSelected(item); 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/Autostart.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.SharedPreferences; 7 | import android.os.Build; 8 | import androidx.preference.PreferenceManager; 9 | 10 | public class Autostart extends BroadcastReceiver { 11 | 12 | @Override 13 | public void onReceive(Context context, Intent intent) { 14 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 15 | 16 | if (( 17 | Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) || 18 | Intent.ACTION_REBOOT.equals(intent.getAction()) || 19 | Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction()) 20 | ) && prefs.getBoolean("bootStart", false) 21 | ) { 22 | Intent i = new Intent(context, MainService.class); 23 | i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 24 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 25 | context.startForegroundService(i); 26 | } else { 27 | context.startService(i); 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/CertServer.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator; 2 | 3 | import android.util.Base64; 4 | import android.util.Log; 5 | 6 | import java.net.DatagramPacket; 7 | import java.net.DatagramSocket; 8 | import java.net.InetAddress; 9 | import java.net.SocketException; 10 | import java.util.Arrays; 11 | 12 | public class CertServer implements Runnable{ 13 | static String TAG = "CertServer"; 14 | static int PORT; 15 | public static String REQUEST = "REQUEST"; 16 | 17 | static Thread serverThread; 18 | static DatagramSocket serverSocket; 19 | static boolean running = false; 20 | 21 | public static void Start(int port) { 22 | PORT = port; 23 | if (serverSocket != null) 24 | serverSocket.close(); 25 | running = true; 26 | serverThread = new Thread(new CertServer()); 27 | serverThread.start(); 28 | } 29 | 30 | public static void Stop() { 31 | //It's a UDP server, it doesn't lock anything so this shouldn't matter 32 | //Close should cancel the receive method 33 | running = false; 34 | if (serverSocket != null) 35 | serverSocket.close(); 36 | } 37 | 38 | public void run() { 39 | try { 40 | serverSocket = new DatagramSocket(PORT); 41 | } catch (Exception e){ 42 | Log.e(TAG, "Failed to start certificate server", e); 43 | return; 44 | } 45 | byte[] receiveData = new byte[1024]; 46 | byte[] cert = Authenticator.getBoxedCertificate(); 47 | byte[] sendData = Base64.encode(cert, Base64.DEFAULT); 48 | while(running) 49 | { 50 | try { 51 | DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); 52 | serverSocket.receive(receivePacket); 53 | byte[] received = Arrays.copyOfRange(receivePacket.getData(), 0, receivePacket.getLength()); 54 | String request = new String(received); 55 | if (request.equals(REQUEST)) 56 | { 57 | InetAddress IPAddress = receivePacket.getAddress(); 58 | int port = receivePacket.getPort(); 59 | DatagramPacket sendPacket = 60 | new DatagramPacket(sendData, sendData.length, IPAddress, port); 61 | serverSocket.send(sendPacket); 62 | Log.d(TAG, "Certificate sent"); 63 | } 64 | } catch (Exception e) { 65 | if (running) { 66 | Log.w(TAG, "Error while running CertServer. Restarting. || " + e.getMessage()); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/LocalBroadcasts.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | 6 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; 7 | 8 | public class LocalBroadcasts { 9 | public static final String ACTION_UPDATE_REMOTES = "update_remotes"; 10 | public static final String ACTION_UPDATE_TRANSFERS = "update_transfers"; 11 | public static final String ACTION_UPDATE_TRANSFER = "update_transfer"; 12 | public static final String ACTION_UPDATE_NETWORK = "update_network"; 13 | public static final String ACTION_DISPLAY_MESSAGE = "display_message"; 14 | public static final String ACTION_DISPLAY_TOAST = "display_toast"; 15 | public static final String ACTION_CLOSE_ALL = "close_all"; 16 | 17 | public static void updateRemotes(Context ctx) { 18 | LocalBroadcastManager.getInstance(ctx).sendBroadcast(new Intent(ACTION_UPDATE_REMOTES)); 19 | } 20 | 21 | public static void updateNetworkState(Context ctx) { 22 | LocalBroadcastManager.getInstance(ctx).sendBroadcast(new Intent(ACTION_UPDATE_NETWORK)); 23 | } 24 | 25 | public static void updateTransfers(Context ctx, String remote) { 26 | Intent intent = new Intent(ACTION_UPDATE_TRANSFERS); 27 | intent.putExtra("remote", remote); 28 | LocalBroadcastManager.getInstance(ctx).sendBroadcast(intent); 29 | } 30 | 31 | public static void updateTransfer(Context ctx, String remote, int id) { 32 | Intent intent = new Intent(ACTION_UPDATE_TRANSFER); 33 | intent.putExtra("remote", remote); 34 | intent.putExtra("id", id); 35 | LocalBroadcastManager.getInstance(ctx).sendBroadcast(intent); 36 | } 37 | 38 | public static void displayMessage(Context ctx, String title, String msg) { 39 | Intent intent = new Intent(ACTION_DISPLAY_MESSAGE); 40 | intent.putExtra("title", title); 41 | intent.putExtra("msg", msg); 42 | LocalBroadcastManager.getInstance(ctx).sendBroadcast(intent); 43 | } 44 | 45 | public static void displayToast(Context ctx, String msg, int length) { 46 | Intent intent = new Intent(ACTION_DISPLAY_TOAST); 47 | intent.putExtra("msg", msg); 48 | intent.putExtra("length", length); 49 | LocalBroadcastManager.getInstance(ctx).sendBroadcast(intent); 50 | } 51 | 52 | public static void closeAll(Context ctx) { 53 | LocalBroadcastManager.getInstance(ctx).sendBroadcast(new Intent(ACTION_CLOSE_ALL)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/MainServiceBinder.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator; 2 | 3 | import android.os.Binder; 4 | 5 | public class MainServiceBinder extends Binder { 6 | 7 | private final MainService service; 8 | 9 | public MainServiceBinder(MainService service) { 10 | this.service = service; 11 | } 12 | 13 | public MainService getService() { 14 | return service; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/RemotesAdapter.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.res.ColorStateList; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.cardview.widget.CardView; 15 | import androidx.recyclerview.widget.RecyclerView; 16 | 17 | public class RemotesAdapter extends RecyclerView.Adapter { 18 | 19 | Activity app; 20 | 21 | public RemotesAdapter(Activity _app) { 22 | app = _app; 23 | } 24 | 25 | @NonNull 26 | @Override 27 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 28 | LayoutInflater inflater = LayoutInflater.from(app); 29 | View view = inflater.inflate(R.layout.remote_view, parent, false); 30 | return new ViewHolder(view); 31 | } 32 | 33 | @Override 34 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 35 | Remote r = MainService.remotes.get(MainService.remotesOrder.get(position)); 36 | setupViewHolder(holder, r); 37 | 38 | holder.cardView.setOnClickListener((view) -> { 39 | Intent i = new Intent(app, TransfersActivity.class); 40 | i.putExtra("remote", r.uuid); 41 | app.startActivity(i); 42 | }); 43 | } 44 | 45 | void setupViewHolder(ViewHolder holder, Remote r) { 46 | holder.txtName.setText(r.displayName); 47 | holder.txtUsername.setText(r.userName + "@" + r.hostname); 48 | holder.txtIP.setText(r.address.getHostAddress() + ":" + r.port); 49 | 50 | Context context = holder.imgProfile.getContext(); 51 | 52 | int color = Utils.getAndroidAttributeColor(context, android.R.attr.textColorSecondary); 53 | 54 | if(r.picture != null) { 55 | holder.imgProfile.setImageTintList(null); 56 | holder.imgProfile.setImageBitmap(r.picture); 57 | } else { 58 | holder.imgProfile.setImageTintList(ColorStateList.valueOf(color)); 59 | } 60 | holder.imgStatus.setImageResource(Utils.getIconForRemoteStatus(r.status)); 61 | if (r.status == Remote.RemoteStatus.ERROR || r.status == Remote.RemoteStatus.DISCONNECTED) { 62 | if (!r.serviceAvailable) 63 | holder.imgStatus.setImageResource(R.drawable.ic_unavailable); 64 | else 65 | color = Utils.getAttributeColor(context.getTheme(), androidx.appcompat.R.attr.colorError); 66 | } 67 | holder.imgStatus.setImageTintList(ColorStateList.valueOf(color)); 68 | holder.imgFav.setVisibility(r.isFavorite() ? View.VISIBLE : View.INVISIBLE); 69 | 70 | holder.cardView.setVisibility(r.errorGroupCode ? View.GONE : View.VISIBLE); 71 | if (r.errorGroupCode) 72 | holder.cardView.setLayoutParams(new ViewGroup.LayoutParams(0, 0)); 73 | else holder.cardView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 74 | } 75 | 76 | @Override 77 | public int getItemCount() { 78 | return MainService.remotes.size(); 79 | } 80 | 81 | public class ViewHolder extends RecyclerView.ViewHolder{ 82 | 83 | CardView cardView; 84 | TextView txtName; 85 | TextView txtUsername; 86 | TextView txtIP; 87 | ImageView imgProfile; 88 | ImageView imgStatus; 89 | ImageView imgFav; 90 | 91 | public ViewHolder(@NonNull View itemView) { 92 | super(itemView); 93 | cardView = itemView.findViewById(R.id.cardView); 94 | txtName = itemView.findViewById(R.id.txtName); 95 | txtUsername = itemView.findViewById(R.id.txtUsername); 96 | txtIP = itemView.findViewById(R.id.txtIP); 97 | imgProfile = itemView.findViewById(R.id.imgProfile); 98 | imgStatus = itemView.findViewById(R.id.imgStatus); 99 | imgFav = itemView.findViewById(R.id.imgFav); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/TileMainService.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.os.Build; 8 | import android.os.IBinder; 9 | import android.service.quicksettings.Tile; 10 | import android.service.quicksettings.TileService; 11 | import android.util.Log; 12 | import android.widget.Toast; 13 | 14 | import androidx.annotation.RequiresApi; 15 | 16 | @RequiresApi(api = Build.VERSION_CODES.N) 17 | public class TileMainService extends TileService implements MainService.RemoteCountObserver { 18 | private Intent serviceIntent; 19 | private ServiceConnection serviceConnection; 20 | private MainServiceBinder binder; 21 | private MainService.DisposableRemoteCountObserver unregisterObserver = null; 22 | 23 | @Override 24 | public void onCreate() { 25 | super.onCreate(); 26 | serviceIntent = new Intent(this, MainService.class); 27 | } 28 | 29 | @Override 30 | public void onStartListening() { 31 | super.onStartListening(); 32 | boolean isRunning = Utils.isMyServiceRunning(this, MainService.class); 33 | updateTile(isRunning); 34 | if (isRunning && binder == null) { 35 | joinService(); 36 | } 37 | } 38 | 39 | @Override 40 | public void onDestroy() { 41 | if (serviceConnection != null) { 42 | unbindService(serviceConnection); 43 | serviceConnection = null; 44 | } 45 | if (unregisterObserver != null) { 46 | unregisterObserver.dispose(); 47 | unregisterObserver = null; 48 | } 49 | binder = null; 50 | super.onDestroy(); 51 | } 52 | 53 | @Override 54 | public void onClick() { 55 | super.onClick(); 56 | switch (getQsTile().getState()) { 57 | case Tile.STATE_ACTIVE: 58 | stopService(); 59 | break; 60 | case Tile.STATE_INACTIVE: 61 | // Activating Warpinator requires unlocking the device first 62 | unlockAndRun(this::startService); 63 | break; 64 | } 65 | } 66 | 67 | void updateTile(boolean active) { 68 | Tile tile = getQsTile(); 69 | if (active) { 70 | tile.setState(Tile.STATE_ACTIVE); 71 | } else { 72 | tile.setState(Tile.STATE_INACTIVE); 73 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 74 | tile.setSubtitle(null); 75 | } 76 | } 77 | tile.updateTile(); 78 | } 79 | 80 | private void updateBoundService(MainServiceBinder binder) { 81 | this.binder = binder; 82 | if (binder == null) { 83 | serviceConnection = null; 84 | if (unregisterObserver != null) { 85 | unregisterObserver.dispose(); 86 | unregisterObserver = null; 87 | } 88 | updateTile(false); 89 | } else { 90 | unregisterObserver = this.binder.getService().observeDeviceCount(this); 91 | updateTile(true); 92 | } 93 | } 94 | 95 | private void startService() { 96 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 97 | try { 98 | startForegroundService(serviceIntent); 99 | } catch (Exception e) { 100 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 101 | Log.e("Tile", "Cannot start main service from background on Android 14+", e); 102 | Toast.makeText(this, "Cannot Warpinator from tile on Android 14+", Toast.LENGTH_LONG).show(); 103 | } 104 | else Log.e("Tile", "Cannot start main service (Android <14)", e); 105 | return; 106 | } 107 | } else { 108 | startService(serviceIntent); 109 | } 110 | joinService(); 111 | } 112 | 113 | private void joinService() { 114 | bindService(serviceIntent, serviceConnection = new ServiceConnection() { 115 | @Override 116 | public void onServiceConnected(ComponentName componentName, IBinder iBinder) { 117 | updateBoundService((MainServiceBinder) iBinder); 118 | } 119 | 120 | @Override 121 | public void onServiceDisconnected(ComponentName componentName) { 122 | updateBoundService(null); 123 | } 124 | }, 0); 125 | } 126 | 127 | private void stopService() { 128 | updateTile(false); 129 | updateBoundService(null); 130 | Intent stopIntent = new Intent(this, MainService.StopSvcReceiver.class); 131 | stopIntent.setAction(MainService.ACTION_STOP); 132 | sendBroadcast(stopIntent); 133 | } 134 | 135 | @Override 136 | public void onDeviceCountChange(int newCount) { 137 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 138 | Tile tile = getQsTile(); 139 | if (newCount > 0 && tile.getState() == Tile.STATE_ACTIVE) { 140 | tile.setSubtitle(getString(R.string.qs_discovered_count, newCount)); 141 | } else { 142 | // If there are no discovered remotes, don't mention them. 143 | // This should fall back to displaying the state of the tile ("On"). 144 | tile.setSubtitle(null); 145 | } 146 | tile.updateTile(); 147 | } 148 | } 149 | 150 | public static void requestListeningState(Context context) { 151 | requestListeningState(context, new ComponentName(context, TileMainService.class)); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/WarpinatorApp.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.util.Log; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.preference.PreferenceManager; 12 | 13 | import com.google.android.material.color.DynamicColors; 14 | 15 | public class WarpinatorApp extends Application implements Application.ActivityLifecycleCallbacks { 16 | static int activitiesRunning = 0; 17 | static final String TAG = "APP"; 18 | 19 | @Override 20 | public void onCreate() { 21 | super.onCreate(); 22 | DynamicColors.applyToActivitiesIfAvailable(this); 23 | registerActivityLifecycleCallbacks(this); 24 | activitiesRunning = 0; 25 | 26 | // Clear old persisted URI permissions (except profile picture) 27 | String picture = PreferenceManager.getDefaultSharedPreferences(this).getString("profile", "0"); 28 | for (var u : getContentResolver().getPersistedUriPermissions()) { 29 | if (u.getUri().toString().equals(picture)) { 30 | Log.v(TAG, "keeping permission for " + u); 31 | continue; 32 | } 33 | Log.v(TAG, "releasing uri permission " + u); 34 | getContentResolver().releasePersistableUriPermission(u.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); 35 | } 36 | } 37 | 38 | @Override 39 | public void onActivityStarted(@NonNull Activity activity) { 40 | Log.d(TAG, "Started activity"); 41 | activitiesRunning++; 42 | MainService.cancelAutoStop(); 43 | } 44 | 45 | @Override 46 | public void onActivityStopped(@NonNull Activity activity) { 47 | activitiesRunning--; 48 | Log.d(TAG, "Stopped activity -> " + activitiesRunning); 49 | if (activitiesRunning < 1) 50 | MainService.scheduleAutoStop(); 51 | } 52 | 53 | @Override public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) { } 54 | @Override public void onActivityResumed(@NonNull Activity activity) {} 55 | @Override public void onActivityPaused(@NonNull Activity activity) {} 56 | @Override public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) { } 57 | @Override public void onActivityDestroyed(@NonNull Activity activity) { } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/ZlibCompressor.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.util.zip.Deflater; 6 | import java.util.zip.Inflater; 7 | 8 | public class ZlibCompressor { 9 | private static final int BUFFER_SIZE = 1024; 10 | 11 | public static byte[] compress(final byte[] input, int length, int level) throws IOException 12 | { 13 | final Deflater deflater = new Deflater(); 14 | deflater.setLevel(level); 15 | deflater.setInput(input, 0, length); 16 | deflater.finish(); 17 | 18 | try (final ByteArrayOutputStream output = new ByteArrayOutputStream(length)) 19 | { 20 | final byte[] buffer = new byte[1024]; 21 | while (!deflater.finished()) 22 | { 23 | final int count = deflater.deflate(buffer); 24 | output.write(buffer, 0, count); 25 | } 26 | 27 | return output.toByteArray(); 28 | } 29 | } 30 | 31 | public static byte[] decompress(final byte[] input) throws Exception 32 | { 33 | final Inflater inflater = new Inflater(); 34 | inflater.setInput(input); 35 | 36 | try (final ByteArrayOutputStream output = new ByteArrayOutputStream(input.length)) 37 | { 38 | byte[] buffer = new byte[BUFFER_SIZE]; 39 | while (!inflater.finished()) 40 | { 41 | final int count = inflater.inflate(buffer); 42 | output.write(buffer, 0, count); 43 | } 44 | 45 | return output.toByteArray(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/preferences/EditTextPreference.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator.preferences; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.EditText; 6 | import android.widget.FrameLayout; 7 | 8 | import androidx.annotation.Nullable; 9 | 10 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 11 | 12 | @SuppressWarnings("unused") 13 | public class EditTextPreference extends androidx.preference.EditTextPreference { 14 | 15 | @Nullable private OnBindEditTextListener mOnBindEditTextListener; 16 | 17 | public EditTextPreference(Context context) { 18 | super(context); 19 | } 20 | 21 | public EditTextPreference(Context context, AttributeSet attrs, int defStyle) { 22 | super(context, attrs, defStyle); 23 | } 24 | 25 | public EditTextPreference(Context context, AttributeSet attrs) { 26 | super(context, attrs); 27 | } 28 | 29 | @Override 30 | public void setOnBindEditTextListener(@Nullable OnBindEditTextListener onBindEditTextListener) { 31 | mOnBindEditTextListener = onBindEditTextListener; 32 | } 33 | 34 | @Override 35 | protected void onClick() { 36 | FrameLayout frameLayout = new FrameLayout(getContext()); 37 | EditText editText = new EditText(getContext()); 38 | 39 | editText.setText(getText()); 40 | 41 | frameLayout.setPaddingRelative(40, 30, 40, 0); 42 | frameLayout.addView(editText); 43 | if (mOnBindEditTextListener != null) { 44 | mOnBindEditTextListener.onBindEditText(editText); 45 | } 46 | 47 | new MaterialAlertDialogBuilder(getContext()).setPositiveButton(android.R.string.ok, (dialog, which) -> { 48 | String newVal = editText.getText().toString(); 49 | if (callChangeListener(newVal)) 50 | setText(newVal); 51 | dialog.dismiss(); 52 | }).setNegativeButton(android.R.string.cancel, null).setTitle(getDialogTitle()).setView(frameLayout).show(); 53 | editText.requestFocus(); 54 | } 55 | 56 | @Override 57 | public CharSequence getSummary() { 58 | return super.getText(); 59 | } 60 | 61 | @Override 62 | public void setText(String text) { 63 | super.setText(text); 64 | this.setSummary(text); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/preferences/ListPreference.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator.preferences; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 7 | 8 | public class ListPreference extends androidx.preference.ListPreference { 9 | public ListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 10 | super(context, attrs, defStyleAttr, defStyleRes); 11 | } 12 | 13 | public ListPreference(Context context, AttributeSet attrs, int defStyleAttr) { 14 | super(context, attrs, defStyleAttr); 15 | } 16 | 17 | public ListPreference(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | public ListPreference(Context context) { 22 | super(context); 23 | } 24 | 25 | @Override 26 | protected void onClick() { 27 | int selected = super.findIndexOfValue((String) super.getValue()); 28 | 29 | new MaterialAlertDialogBuilder(getContext()) 30 | .setNegativeButton(android.R.string.cancel, null) 31 | .setTitle(getDialogTitle()).setSingleChoiceItems( 32 | getEntries(), 33 | selected, 34 | (dialog, which) -> { 35 | super.setValueIndex(which); 36 | getOnPreferenceChangeListener().onPreferenceChange(this, super.getValue()); 37 | dialog.dismiss(); 38 | } 39 | ).show(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/preferences/ProfilePicturePreference.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator.preferences; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.graphics.Bitmap; 7 | import android.graphics.BitmapFactory; 8 | import android.net.Uri; 9 | import android.util.AttributeSet; 10 | import android.util.Log; 11 | import android.view.View; 12 | import android.widget.GridLayout; 13 | import android.widget.ImageButton; 14 | import android.widget.ImageView; 15 | import android.widget.Toast; 16 | 17 | import androidx.activity.result.ActivityResultLauncher; 18 | import androidx.activity.result.contract.ActivityResultContracts; 19 | import androidx.preference.Preference; 20 | 21 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 22 | 23 | import slowscript.warpinator.R; 24 | import slowscript.warpinator.Server; 25 | import slowscript.warpinator.SettingsActivity; 26 | 27 | public class ProfilePicturePreference extends Preference { 28 | 29 | ActivityResultLauncher customImageActivityResultLauncher; 30 | private final Context mContext; 31 | 32 | public ProfilePicturePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 33 | super(context, attrs, defStyleAttr, defStyleRes); 34 | mContext = context; 35 | registerForResult(); 36 | } 37 | 38 | public ProfilePicturePreference(Context context, AttributeSet attrs, int defStyleAttr) { 39 | super(context, attrs, defStyleAttr); 40 | mContext = context; 41 | registerForResult(); 42 | } 43 | 44 | public ProfilePicturePreference(Context context, AttributeSet attrs) { 45 | super(context, attrs); 46 | mContext = context; 47 | registerForResult(); 48 | } 49 | 50 | public ProfilePicturePreference(Context context) { 51 | super(context); 52 | mContext = context; 53 | registerForResult(); 54 | } 55 | 56 | @Override 57 | protected void onClick() { 58 | View view = View.inflate(mContext, R.layout.profile_chooser_view, null); 59 | 60 | GridLayout layout = view.findViewById(R.id.layout1); 61 | ImageView imgCurrent = view.findViewById(R.id.imgCurrent); 62 | 63 | String picture = getSharedPreferences().getString("profile", "0"); 64 | imgCurrent.setImageBitmap(Server.getProfilePicture(picture, mContext)); 65 | 66 | for (int i = 0; i < 12; i++) { 67 | final int idx = i; 68 | ImageButton btn = new ImageButton(mContext); 69 | btn.setImageBitmap(Server.getProfilePicture(String.valueOf(idx), mContext)); 70 | btn.setOnClickListener((v) -> { 71 | getSharedPreferences().edit().putString("profile", String.valueOf(idx)).apply(); 72 | imgCurrent.setImageBitmap(Server.getProfilePicture(String.valueOf(idx), mContext)); 73 | }); 74 | layout.addView(btn); 75 | } 76 | 77 | MaterialAlertDialogBuilder materialAlertDialogBuilder = new MaterialAlertDialogBuilder(mContext) 78 | .setTitle(R.string.picture_settings_title) 79 | .setNegativeButton(android.R.string.cancel, null) 80 | .setPositiveButton(android.R.string.ok, null) 81 | .setNeutralButton(R.string.custom, (dialog, which) -> { 82 | Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT); 83 | i.addCategory(Intent.CATEGORY_OPENABLE); 84 | i.setType("image/*"); 85 | i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 86 | i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); 87 | 88 | customImageActivityResultLauncher.launch(i); 89 | dialog.dismiss(); 90 | }).setView(view); 91 | materialAlertDialogBuilder.show(); 92 | } 93 | 94 | private void registerForResult() { 95 | customImageActivityResultLauncher = getActivity().registerForActivityResult( 96 | new ActivityResultContracts.StartActivityForResult(), 97 | result -> { 98 | if (result.getResultCode() == Activity.RESULT_OK) { 99 | Intent data = result.getData(); 100 | if (data != null) { 101 | Uri u = data.getData(); 102 | if (u != null) { 103 | try (var is = mContext.getContentResolver().openInputStream(u)) { 104 | var bmp = BitmapFactory.decodeStream(is); 105 | // Save "profilePic" to private app storage in reduced resolution 106 | int outW, outH; 107 | int maxDim = Math.min(512, Math.max(bmp.getWidth(), bmp.getHeight())); 108 | if(bmp.getWidth() > bmp.getHeight()){ 109 | outW = maxDim; 110 | outH = (bmp.getHeight() * maxDim) / bmp.getWidth(); 111 | } else { 112 | outH = maxDim; 113 | outW = (bmp.getWidth() * maxDim) / bmp.getHeight(); 114 | } 115 | Bitmap resized = Bitmap.createScaledBitmap(bmp, outW, outH, true); 116 | try (var os = mContext.openFileOutput("profilePic.png", Context.MODE_PRIVATE)) { 117 | //quality is irrelevant for PNG 118 | resized.compress(Bitmap.CompressFormat.PNG, 100, os); 119 | } 120 | getSharedPreferences().edit().putString("profile", "profilePic.png").apply(); 121 | } catch (Exception e) { 122 | Log.e("ProfilePic", "Failed to save profile picture: " + u, e); 123 | Toast.makeText(mContext, "Failed to save profile picture: " + e, Toast.LENGTH_LONG).show(); 124 | } 125 | } 126 | } 127 | } 128 | }); 129 | } 130 | 131 | private SettingsActivity getActivity() { 132 | return (SettingsActivity) mContext; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/slowscript/warpinator/preferences/ResetablePreference.java: -------------------------------------------------------------------------------- 1 | package slowscript.warpinator.preferences; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.widget.Button; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | import androidx.preference.PreferenceViewHolder; 11 | 12 | import slowscript.warpinator.R; 13 | 14 | public class ResetablePreference extends androidx.preference.Preference { 15 | private Button resetBtn; 16 | private boolean resetEnabled = true; 17 | private View.OnClickListener onResetListener; 18 | 19 | public ResetablePreference(@NonNull Context context, @Nullable AttributeSet attrs) { 20 | super(context, attrs); 21 | setLayoutResource(R.layout.resetable_preference); 22 | } 23 | 24 | public void setOnResetListener(View.OnClickListener listener) { 25 | onResetListener = listener; 26 | if (resetBtn != null) 27 | resetBtn.setOnClickListener(listener); 28 | } 29 | 30 | public void setResetEnabled(boolean enabled) { 31 | resetEnabled = enabled; 32 | if (resetBtn != null) 33 | resetBtn.setEnabled(enabled); 34 | } 35 | 36 | @Override 37 | public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { 38 | super.onBindViewHolder(holder); 39 | resetBtn = (Button)holder.itemView.findViewById(R.id.resetButton); 40 | resetBtn.setOnClickListener(onResetListener); 41 | resetBtn.setEnabled(resetEnabled); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/proto/warp.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_package = "slowscript.warpinator"; 4 | option java_outer_classname = "WarpProto"; 5 | 6 | // ************ Important! *************** 7 | // 8 | // If you change anything here, you *must* run 'generate-protobuf' to update the 9 | // generated stub files. 10 | // 11 | // Never change the existing members and member values of messages, only add new ones. 12 | 13 | service Warp { 14 | // Sender methods 15 | // api v1 duplex method (ping style) 16 | rpc CheckDuplexConnection(LookupName) returns (HaveDuplex) {} 17 | // api v2 duplex method (block/future) 18 | rpc WaitingForDuplex(LookupName) returns (HaveDuplex) {} 19 | 20 | rpc GetRemoteMachineInfo(LookupName) returns (RemoteMachineInfo) {} 21 | rpc GetRemoteMachineAvatar(LookupName) returns (stream RemoteMachineAvatar) {} 22 | rpc ProcessTransferOpRequest(TransferOpRequest) returns (VoidType) {} 23 | rpc PauseTransferOp(OpInfo) returns (VoidType) {} 24 | 25 | // Receiver methods 26 | rpc StartTransfer(OpInfo) returns (stream FileChunk) {} 27 | 28 | // Both 29 | rpc CancelTransferOpRequest(OpInfo) returns (VoidType) {} 30 | rpc StopTransfer(StopInfo) returns (VoidType) {} 31 | 32 | rpc Ping(LookupName) returns (VoidType) {} 33 | } 34 | 35 | message RemoteMachineInfo { 36 | string display_name = 1; 37 | string user_name = 2; 38 | } 39 | 40 | message RemoteMachineAvatar { 41 | bytes avatar_chunk = 1; 42 | } 43 | 44 | message LookupName { 45 | string id = 1; 46 | string readable_name = 2; 47 | } 48 | 49 | message HaveDuplex { 50 | bool response = 2; 51 | } 52 | 53 | message VoidType { 54 | int32 dummy = 1; 55 | } 56 | 57 | message OpInfo { 58 | string ident = 1; 59 | uint64 timestamp = 2; 60 | string readable_name = 3; 61 | bool use_compression = 4; 62 | } 63 | 64 | message StopInfo { 65 | OpInfo info = 1; 66 | bool error = 2; 67 | } 68 | 69 | message TransferOpRequest { 70 | OpInfo info = 1; 71 | string sender_name = 2; 72 | string receiver_name = 3; 73 | string receiver = 4; // don't need for now 74 | uint64 size = 5; 75 | uint64 count = 6; 76 | string name_if_single = 7; 77 | string mime_if_single = 8; 78 | repeated string top_dir_basenames = 9; 79 | } 80 | 81 | message FileChunk { 82 | string relative_path = 1; 83 | int32 file_type = 2; 84 | string symlink_target = 3; 85 | bytes chunk = 4; 86 | uint32 file_mode = 5; 87 | FileTime time = 6; 88 | } 89 | 90 | message FileTime { 91 | uint64 mtime = 1; 92 | uint32 mtime_usec = 2; 93 | } 94 | 95 | service WarpRegistration { 96 | rpc RequestCertificate(RegRequest) returns (RegResponse) {} 97 | rpc RegisterService(ServiceRegistration) returns (ServiceRegistration) {} 98 | } 99 | 100 | message RegRequest { 101 | string ip = 1; 102 | string hostname = 2; 103 | } 104 | 105 | message RegResponse { 106 | string locked_cert = 1; 107 | } 108 | 109 | message ServiceRegistration { 110 | string service_id = 1; 111 | string ip = 2; 112 | uint32 port = 3; 113 | string hostname = 4; 114 | uint32 api_version = 5; 115 | uint32 auth_port = 6; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /app/src/main/res/anim/anim_null.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/anim/anim_push_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/anim/anim_push_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slowscript/warpinator-android/3a13e402f1f547a9737478297656c6688614d05c/app/src/main/res/drawable-hdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slowscript/warpinator-android/3a13e402f1f547a9737478297656c6688614d05c/app/src/main/res/drawable-mdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/transfers_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v31/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 17 | 24 | 31 | 38 | 45 | 52 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slowscript/warpinator-android/3a13e402f1f547a9737478297656c6688614d05c/app/src/main/res/drawable-xhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slowscript/warpinator-android/3a13e402f1f547a9737478297656c6688614d05c/app/src/main/res/drawable-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slowscript/warpinator-android/3a13e402f1f547a9737478297656c6688614d05c/app/src/main/res/drawable-xxxhdpi/ic_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_accept.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_decline.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_download.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 17 | 24 | 31 | 38 | 45 | 52 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_meh.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_reconnect.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_remove_items.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_empty.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_full.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star_toggle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_awaiting_duplex.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 24 | 28 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_connected.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 27 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_status_connecting.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_stop.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_unavailable.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_upload.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_user.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_warpinator.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 20 | 27 | 34 | 41 | 48 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_corners.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/transfers_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 13 | 14 | 26 | 27 | 39 | 40 | 50 | 51 | 63 | 64 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 23 | 24 | 38 | 39 | 48 | 49 | 59 | 60 | 69 | 70 | 77 | 78 | 88 | 89 | 90 | 91 | 100 | 101 | 110 | 111 | 120 | 121 | 122 | 130 | 131 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_share.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 26 | 27 | 36 | 37 | 47 | 48 | 57 | 58 | 59 | 68 | 69 | 78 | 79 | 88 | 89 | 90 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_transfers.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 |