From 33eb8ac4577198c7348aecd327e5e3fa2d17cb8d Mon Sep 17 00:00:00 2001 From: Daniel C Date: Mon, 1 Jan 2024 00:37:29 -0500 Subject: [PATCH] Major refactor - out with MyWiFiComm, use camlib.SimpleSocket instead Also - sneaky - I added libui-android - eventually I will be able to start writing new parts of the app in C rather than fooling around with Android's crap --- .idea/.name | 2 +- Makefile | 13 +- app/src/main/AndroidManifest.xml | 6 +- .../petabyt => }/camlib/CamlibBackend.java | 4 +- app/src/main/java/camlib/SimpleSocket.java | 110 + .../{dev/petabyt => }/camlib/UsbComm.java | 2 +- app/src/main/java/camlib/WiFiComm.java | 93 + .../java/dev/danielc/fujiapp/Backend.java | 85 +- .../java/dev/danielc/fujiapp/Gallery.java | 56 +- .../main/java/dev/danielc/fujiapp/LibU.java | 118 + .../dev/danielc/fujiapp/MainActivity.java | 70 +- .../java/dev/danielc/fujiapp/MyWiFiComm.java | 145 - .../main/java/dev/danielc/fujiapp/Tester.java | 61 +- .../main/java/dev/danielc/fujiapp/Viewer.java | 161 +- .../java/dev/petabyt/camlib/WiFiComm.java | 181 - app/src/main/java/libui/LibUI.java | 514 +++ .../res/drawable/baseline_content_copy_24.xml | 5 + app/src/main/res/drawable/grey_button.xml | 21 + app/src/main/res/layout/activity_main.xml | 9 +- lib/Android.mk | 4 +- lib/backend.h | 19 +- lib/fuji.c | 24 +- lib/fuji.h | 2 + lib/fujiptp.h | 3 + lib/jni.c | 2 + lib/libui.c | 412 ++ lib/main.c | 79 +- lib/myjni.h | 8 +- lib/net.c | 104 +- lib/ptp.c | 26 + lib/scripts.c | 28 + lib/tester.c | 6 +- lib/ui.h | 4015 +++++++++++++++++ lib/ui_android.h | 90 + lib/viewer.c | 22 + settings.gradle | 2 +- 36 files changed, 5918 insertions(+), 584 deletions(-) rename app/src/main/java/{dev/petabyt => }/camlib/CamlibBackend.java (97%) create mode 100644 app/src/main/java/camlib/SimpleSocket.java rename app/src/main/java/{dev/petabyt => }/camlib/UsbComm.java (96%) create mode 100644 app/src/main/java/camlib/WiFiComm.java create mode 100644 app/src/main/java/dev/danielc/fujiapp/LibU.java delete mode 100644 app/src/main/java/dev/danielc/fujiapp/MyWiFiComm.java delete mode 100644 app/src/main/java/dev/petabyt/camlib/WiFiComm.java create mode 100644 app/src/main/java/libui/LibUI.java create mode 100644 app/src/main/res/drawable/baseline_content_copy_24.xml create mode 100644 app/src/main/res/drawable/grey_button.xml create mode 100644 lib/libui.c create mode 100644 lib/ptp.c create mode 100644 lib/scripts.c create mode 100644 lib/ui.h create mode 100644 lib/ui_android.h create mode 100644 lib/viewer.c diff --git a/.idea/.name b/.idea/.name index d57a588..5970dba 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -Fuji Wifi App \ No newline at end of file +Fudge \ No newline at end of file diff --git a/Makefile b/Makefile index 7ae96c5..2ce86f2 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,18 @@ PKG=dev.danielc.fujiapp install: - bash gradlew :app:buildCMakeDebug[arm64-v8a] installDebug -Pandroid.optional.compilation=INSTANT_DEV -Pandroid.injected.build.api=24 - #bash gradlew installDebug -Pandroid.optional.compilation=INSTANT_DEV -Pandroid.injected.build.api=24 + #bash gradlew :app:buildCMakeDebug[arm64-v8a] installDebug -Pandroid.optional.compilation=INSTANT_DEV -Pandroid.injected.build.api=24 + bash gradlew installDebug -Pandroid.optional.compilation=INSTANT_DEV -Pandroid.injected.build.api=24 adb shell monkey -p $(PKG) -c android.intent.category.LAUNCHER 1 log: adb logcat | grep -F "`adb shell ps | grep $(PKG) | tr -s [:space:] ' ' | cut -d' ' -f2`" + +ln: + rm -f app/src/main/java/camlib/*.java + cd app/src/main/java/camlib/ && ln ../../../../../../camlibjava/*.java . + + rm -f app/src/main/java/libui/*.java + cd app/src/main/java/libui/ && ln ../../../../../../libui-android/*.java . + + cd lib && ln ../../libui-android/*.c ../../libui-android/*.h . diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b0e715..3e0b170 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,15 +24,19 @@ android:exported="false" /> @@ -42,4 +46,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/dev/petabyt/camlib/CamlibBackend.java b/app/src/main/java/camlib/CamlibBackend.java similarity index 97% rename from app/src/main/java/dev/petabyt/camlib/CamlibBackend.java rename to app/src/main/java/camlib/CamlibBackend.java index 2291003..8b93de8 100644 --- a/app/src/main/java/dev/petabyt/camlib/CamlibBackend.java +++ b/app/src/main/java/camlib/CamlibBackend.java @@ -1,6 +1,6 @@ // Basic backend parent class for camlib with JNI // Copyright Daniel Cook - Apache License -package dev.petabyt.camlib; +package camlib; public class CamlibBackend { // Integer error exception - see camlib PTP_ error codes @@ -34,4 +34,4 @@ public PtpErr(int code) { public static final int PTP_LV_EOS = 1; public static final int PTP_LV_CANON = 2; public static final int PTP_LV_ML = 3; -}; \ No newline at end of file +}; diff --git a/app/src/main/java/camlib/SimpleSocket.java b/app/src/main/java/camlib/SimpleSocket.java new file mode 100644 index 0000000..32b523e --- /dev/null +++ b/app/src/main/java/camlib/SimpleSocket.java @@ -0,0 +1,110 @@ +package camlib; + +import android.net.ConnectivityManager; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; + +import dev.danielc.fujiapp.Backend; + +public class SimpleSocket { + private static ConnectivityManager m = null; + public static void setConnectivityManager(ConnectivityManager m) { + SimpleSocket.m = m; + } + + public int port; + public String ip; + public boolean alive; + + int timeout = 2000; + + Socket socket; + InputStream inputStream; + OutputStream outputStream; + + public String failReason; + + private byte[] buffer = null; + private int bufferSize = 512; + + public SimpleSocket() { + this.buffer = new byte[bufferSize]; + } + + public Object getBuffer() { + return this.buffer; + } + + public int getBufferSize() { + return this.bufferSize; + } + + public void connectWiFi(String ip, int port) throws Exception { + Socket s; + try { + s = WiFiComm.connectWiFiSocket(m, ip, port); + } catch (Exception e) { + throw e; + } + + this.ip = ip; + this.port = port; + + s.setSoTimeout(timeout); + this.inputStream = s.getInputStream(); + this.outputStream = s.getOutputStream(); + + alive = true; + } + + public int read(int size) { + try { + return inputStream.read(buffer, 0, size); + } catch (IOException e) { + failReason = e.toString(); + alive = false; + return -1; + } + } + + public int write(int size) { + try { + outputStream.write(buffer, 0, size); + outputStream.flush(); + return size; + } catch (IOException e) { + failReason = e.toString(); + alive = false; + return -1; + } + } + + public void close() { + alive = false; + try { + // Suck the remaining bytes out of socket + byte[] remaining = new byte[100]; + inputStream.read(remaining); + } catch (Exception e) { + // I don't care + } + + try { + if (socket != null) { + socket.close(); + } + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + } catch (IOException e) { + Backend.print("Socket close error: " + e.getMessage()); + } + } +} diff --git a/app/src/main/java/dev/petabyt/camlib/UsbComm.java b/app/src/main/java/camlib/UsbComm.java similarity index 96% rename from app/src/main/java/dev/petabyt/camlib/UsbComm.java rename to app/src/main/java/camlib/UsbComm.java index 4f85539..f7fe552 100644 --- a/app/src/main/java/dev/petabyt/camlib/UsbComm.java +++ b/app/src/main/java/camlib/UsbComm.java @@ -1,6 +1,6 @@ // Basic libusb-like driver intented for PTP devices // Copyright Daniel Cook - Apache License -package dev.petabyt.camlib; +package camlib; import android.app.PendingIntent; import android.content.Context; diff --git a/app/src/main/java/camlib/WiFiComm.java b/app/src/main/java/camlib/WiFiComm.java new file mode 100644 index 0000000..271e6f2 --- /dev/null +++ b/app/src/main/java/camlib/WiFiComm.java @@ -0,0 +1,93 @@ +// Basic wifi-priority socket interface for camlib +// Copyright Daniel Cook - Apache License +package camlib; + +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkRequest; +import android.os.Build; +import android.util.Log; +import java.net.InetSocketAddress; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.ArrayList; + +import dev.danielc.fujiapp.Backend; +import libui.LibUI; + +public class WiFiComm { + public static final String TAG = "camlib"; + + public boolean killSwitch = true; + + static Network wifiDevice = null; + + static Socket tryConnectToSocket(Network net, String ip, int port) throws Exception { + Socket sock; + try { + // Create and connect to socket + sock = new Socket(); + + // Bind socket to the network device we selected + net.bindSocket(sock); + + //sock.setKeepAlive(true); + sock.setTcpNoDelay(true); + sock.setReuseAddress(true); + + sock.connect(new InetSocketAddress(ip, port), 1000); + } catch (SocketTimeoutException e) { + Log.d(TAG, e.toString()); + throw new Exception("Connection timed out"); + } catch (Exception e) { + Log.d(TAG, e.toString()); + throw new Exception("Failed to connect."); + } + + return sock; + } + + public static void startNetworkListeners(ConnectivityManager connectivityManager) { + NetworkRequest.Builder requestBuilder = new NetworkRequest.Builder(); + requestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + Log.d(TAG, "Wifi network is available"); + wifiDevice = network; + } + @Override + public void onLost(Network network) { + Log.e(TAG, "Lost network\n"); + wifiDevice = null; + } + @Override + public void onUnavailable() { + Log.e(TAG, "Network unavailable\n"); + wifiDevice = null; + } + }; + + connectivityManager.requestNetwork(requestBuilder.build(), networkCallback); + } + + public static Socket connectWiFiSocket(ConnectivityManager connectivityManager, String ip, int port) throws Exception { + NetworkInfo wifiInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + if (!wifiInfo.isAvailable()) { + throw new Exception("WiFi is not available."); + } else if (!wifiInfo.isConnected()) { + throw new Exception("Not connected to a WiFi network."); + } + + if (wifiDevice == null) { + throw new Exception("Not connected to WiFi."); + } + + return tryConnectToSocket(wifiDevice, ip, port); + } +} diff --git a/app/src/main/java/dev/danielc/fujiapp/Backend.java b/app/src/main/java/dev/danielc/fujiapp/Backend.java index 4cd4a8e..0ee37fe 100644 --- a/app/src/main/java/dev/danielc/fujiapp/Backend.java +++ b/app/src/main/java/dev/danielc/fujiapp/Backend.java @@ -2,40 +2,73 @@ // Copyright 2023 Daniel C - https://github.com/petabyt/fujiapp package dev.danielc.fujiapp; +import android.content.Context; +import android.net.ConnectivityManager; import android.util.Log; import android.os.Environment; import java.io.File; import org.json.JSONObject; import java.util.Arrays; -import dev.petabyt.camlib.*; +import camlib.*; public class Backend extends CamlibBackend { static { System.loadLibrary("fujiapp"); } - public static MyWiFiComm wifi; + static SimpleSocket cmdSocket = new SimpleSocket(); + static SimpleSocket eventSocket = new SimpleSocket(); + static SimpleSocket videoSocket = new SimpleSocket(); - // Block all communication in UsbComm and WiFiComm - // Write reason + code, and reconnect popup - public static void reportError(int code, String reason) { - if (wifi.killSwitch == false) { - wifi.killSwitch = true; + public static void fujiConnectToCmd() throws Exception { + Backend.print("Connecting..."); - if (reason != null) { - print("Disconnect: " + reason); + try { + cmdSocket.connectWiFi(Backend.FUJI_IP, Backend.FUJI_CMD_PORT); + } catch (Exception e) { + + if (BuildConfig.DEBUG) { + Backend.print("Trying emulator IP"); + try { + cmdSocket.connectWiFi(Backend.FUJI_EMU_IP, Backend.FUJI_CMD_PORT); + } catch (Exception e2) { + throw e2; + } + } else { + throw e; } + } + } - Backend.wifi.close(); + public static void fujiConnectEventAndVideo() throws Exception { + String ip = cmdSocket.ip; + try { + eventSocket.connectWiFi(ip, Backend.FUJI_EVENT_PORT); + } catch (Exception e) { + Backend.print("Failed to connect to event socket: " + e.toString()); + throw e; } + + try { + videoSocket.connectWiFi(ip, Backend.FUJI_VIDEO_PORT); + } catch (Exception e) { + Backend.print("Failed to connect to video socket: " + e.toString()); + throw e; + } + } + + // Block all communication in UsbComm and WiFiComm + // Write reason + code, and reconnect popup + public native static void cReportError(int code, String reason); + public static void reportError(int code, String reason) { + cReportError(code, reason); } // In order to give the backend access to the static methods, new objects must be made private static boolean haveInited = false; public static void init() { if (haveInited == false) { - wifi = new MyWiFiComm(); - cInit(new Backend(), wifi); + cInit(new Backend(), cmdSocket); } haveInited = true; } @@ -52,14 +85,13 @@ public static void clear() { public static final int FUJI_EVENT_PORT = 55741; public static final int FUJI_VIDEO_PORT = 55742; public static final int OPEN_TIMEOUT = 1000; - public static final int TIMEOUT = 2000; // Note: 'synchronized' means only one of these methods can be used at time - // java's version of a mutex - public native synchronized static void cInit(Backend b, MyWiFiComm c); + public native synchronized static void cInit(Backend b, SimpleSocket c); public native synchronized static int cPtpFujiInit(); public native synchronized static int cPtpFujiPing(); - public native synchronized static int cPtpGetPropValue(int code); + //public native synchronized static int cPtpGetPropValue(int code); public native synchronized static int cPtpFujiWaitUnlocked(); public native synchronized static int cFujiConfigVersion(); public native synchronized static int cFujiConfigInitMode(); @@ -82,16 +114,20 @@ public static void clear() { // For test suite only public native synchronized static void cTesterInit(Tester t); - public native synchronized static String cTestFunc(); + //public native synchronized static String cTestFunc(); public native synchronized static int cFujiTestSetupImageGallery(); - public native synchronized static int cTestStuff(); + //public native synchronized static int cTestStuff(); public native synchronized static int cFujiTestSuiteSetup(); // Enable disable verbose logging to file public native synchronized static int cRouteLogs(String filename); - public native synchronized static void cEndLogs(); + public native synchronized static String cEndLogs(); + + //public native static boolean cIsUsingEmulator(); - public native static boolean cIsUsingEmulator(); + public native static int cFujiScriptsScreen(Context ctx); + + public native static int cSetProgressBar(Object progressBar); // Runs a request with integer parameters public static JSONObject run(String req, int[] arr) throws Exception { @@ -139,7 +175,7 @@ public static JSONObject fujiGetUncompressedObjectInfo(int handle) throws Except public static void clearPrint() { basicLog = ""; - MainActivity.getInstance().setErrorText(""); + updateLog(); } // debug function for both Java frontend and JNI backend @@ -158,11 +194,10 @@ public static void print(String arg) { } public static void updateLog() { - if (MainActivity.getInstance() != null) { - MainActivity.getInstance().setErrorText(basicLog); - } - if (Gallery.getInstance() != null) { - Gallery.getInstance().setErrorText(basicLog); + if (logLocation == "main") { + MainActivity.instance.setLogText(basicLog); + } else if (logLocation == "gallery") { + Gallery.instance.setLogText(basicLog); } } diff --git a/app/src/main/java/dev/danielc/fujiapp/Gallery.java b/app/src/main/java/dev/danielc/fujiapp/Gallery.java index 9ca2a10..1e2e7a3 100644 --- a/app/src/main/java/dev/danielc/fujiapp/Gallery.java +++ b/app/src/main/java/dev/danielc/fujiapp/Gallery.java @@ -12,7 +12,6 @@ import android.net.ConnectivityManager; import android.view.MenuItem; import android.view.View; -import android.widget.Toast; import android.content.Intent; import android.os.Bundle; import android.os.Handler; @@ -20,25 +19,22 @@ import android.widget.TextView; public class Gallery extends AppCompatActivity { - private static Gallery instance; + public static Gallery instance; final int GRID_SIZE = 4; - public static Gallery getInstance() { - return instance; - } - private RecyclerView recyclerView; private ImageAdapter imageAdapter; Handler handler; - public void setErrorText(String arg) { + public void setLogText(String arg) { handler.post(new Runnable() { @Override public void run() { - TextView error_msg = findViewById(R.id.gallery_logs); - error_msg.setText(arg); + TextView tv = findViewById(R.id.gallery_logs); + if (tv == null) return; + tv.setText(arg); } }); } @@ -63,6 +59,29 @@ public void run() { } } + private void downloadSelectedImages() { + /* + showWarning("selected image downloading is in development."); + + try { + JSONObject jsonObject = Camera.getObjectInfo(handle); + } catch (Exception e) { + + } + + filename = jsonObject.getString("filename"); + int size = jsonObject.getInt("compressedSize"); + int imgX = jsonObject.getInt("imgWidth"); + int imgY = jsonObject.getInt("imgHeight"); + + // GetObjectInfo - uncompressed, so need to guess buffer size + // GetObject - get the entire object into RAM + // GetEvents - if failed, end of stream + + //Viewer.writeFile() + */ + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -118,8 +137,11 @@ public void run() { } if (Backend.cIsMultipleMode()) { - showWarning("Multiple/single import is unsupported, don't expect it to work."); - } else if (Backend.cIsUntestedMode()) { + showWarning("View multiple mode in development"); + downloadSelectedImages(); + } + + if (Backend.cIsUntestedMode()) { showWarning("This camera is untested, support is under development."); } @@ -139,7 +161,9 @@ public void run() { return; } - if (Backend.wifi.fujiConnectEventAndVideo(m)) { + try { + Backend.fujiConnectEventAndVideo(); + } catch (Exception e) { Backend.reportError(Backend.PTP_RUNTIME_ERR, "Failed to enter remote mode"); return; } @@ -188,11 +212,12 @@ public void run() { @Override public void run() { // Do nothing if connection doesn't exist anymore - if (Backend.wifi.killSwitch) return; + if (!Backend.cmdSocket.alive) return; Intent intent = new Intent(Gallery.this, MainActivity.class); startActivity(intent); - Backend.reportError(Backend.PTP_IO_ERR, "Failed to ping camera, disconnected"); + Backend.logLocation = "main"; + Backend.reportError(Backend.PTP_IO_ERR, "Failed to ping camera"); } }); return; @@ -207,12 +232,15 @@ public void run() { @Override public void onBackPressed() { // TODO: Press again to terminate connection + //Backend.logLocation = "main"; + //finish(); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: + Backend.logLocation = "main"; Backend.reportError(Backend.PTP_OK, "Graceful disconnect"); finish(); return true; diff --git a/app/src/main/java/dev/danielc/fujiapp/LibU.java b/app/src/main/java/dev/danielc/fujiapp/LibU.java new file mode 100644 index 0000000..abef026 --- /dev/null +++ b/app/src/main/java/dev/danielc/fujiapp/LibU.java @@ -0,0 +1,118 @@ +package libui; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Environment; +import android.widget.Toast; + +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Scanner; + +public class LibU { + public static void createDir(String directoryPath) { + File directory = new File(directoryPath); + if (!directory.exists()) { + if (!directory.mkdirs()) { + return; + } + } + } + + public static void toast(Context ctx, String arg) { + Toast.makeText(ctx, arg, Toast.LENGTH_SHORT).show(); + } + + public static void shareJpeg(Context ctx, String path, String action) { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("image/jpeg"); + + Uri imageUri = Uri.parse("file://" + path); + shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri); + + Intent chooserIntent = Intent.createChooser(shareIntent, action); + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (chooserIntent.resolveActivity(ctx.getPackageManager()) != null) { + ctx.startActivity(chooserIntent); + } + } + + public static void writeFile(String path, byte[] data) throws Exception { + File file = new File(path); + FileOutputStream fos = null; + + try { + fos = new FileOutputStream(file); + fos.write(data); + } catch (IOException e) { + e.printStackTrace(); + throw e; + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + throw e; + } + } + } + } + + public void getFilePermissions(Activity ctx) { + // Require legacy Android write permissions + if (ContextCompat.checkSelfPermission(ctx, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(ctx, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); + } + } + + public static String getDCIM() { + String mainStorage = Environment.getExternalStorageDirectory().getAbsolutePath(); + return mainStorage + File.separator + "DCIM" + File.separator; + } + + public static String getDocuments() { + String mainStorage = Environment.getExternalStorageDirectory().getAbsolutePath(); + return mainStorage + File.separator + "Documents" + File.separator; + } + + public static JSONObject getJSONSettings(Activity ctx, String key) throws Exception { + SharedPreferences prefs = ctx.getSharedPreferences(ctx.getPackageName(), Context.MODE_PRIVATE); + + String value = prefs.getString(ctx.getPackageName() + "." + key, null); + if (value == null) return null; + + return new JSONObject(value); + } + + public static void storeJSONSettings(Activity ctx, String key, String value) throws Exception { + SharedPreferences prefs = ctx.getSharedPreferences(ctx.getPackageName(), Context.MODE_PRIVATE); + prefs.edit().putString(ctx.getPackageName() + "." + key, value).apply(); + } + + public static String readFileFromAssets(Context ctx, String file) throws Exception { + try { + InputStream inputStream = ctx.getAssets().open(file); + Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); + String fileContent = scanner.hasNext() ? scanner.next() : ""; + scanner.close(); + return fileContent; + } catch (IOException e) { + e.printStackTrace(); + throw e; + } + } +} diff --git a/app/src/main/java/dev/danielc/fujiapp/MainActivity.java b/app/src/main/java/dev/danielc/fujiapp/MainActivity.java index 3ba20de..934c262 100644 --- a/app/src/main/java/dev/danielc/fujiapp/MainActivity.java +++ b/app/src/main/java/dev/danielc/fujiapp/MainActivity.java @@ -6,14 +6,10 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkRequest; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.widget.TextView; @@ -21,9 +17,13 @@ import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -public class MainActivity extends AppCompatActivity { - private static MainActivity instance; +import camlib.SimpleSocket; +import camlib.WiFiComm; +import libui.LibU; +import libui.LibUI; +public class MainActivity extends AppCompatActivity { + public static MainActivity instance; Handler handler; @Override @@ -33,8 +33,9 @@ protected void onCreate(Bundle savedInstanceState) { instance = this; handler = new Handler(Looper.getMainLooper()); - Backend.init(); + LibUI.buttonBackgroundResource = R.drawable.grey_button; + Backend.init(); Backend.updateLog(); findViewById(R.id.reconnect).setOnClickListener(new View.OnClickListener() { @@ -53,6 +54,13 @@ public void onClick(View v) { } }); + findViewById(R.id.scripts).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Backend.cFujiScriptsScreen(MainActivity.this); + } + }); + ((TextView)findViewById(R.id.bottomText)).setText(getString(R.string.url) + "\n" + "Download location: " + Backend.getDownloads() + "\n" + getString(R.string.motd_thing)); @@ -70,49 +78,49 @@ public void onClick(View v) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } - // TODO: Show status on screen - MyWiFiComm.startNetworkListeners((ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE)); - } + ConnectivityManager m = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); - @Override - public void onBackPressed() { - Backend.logLocation = "main"; + SimpleSocket.setConnectivityManager(m); + + // Idea: Show WiFi status on screen? + WiFiComm.startNetworkListeners(m); } public void connectClick(View v) { - if (Backend.cIsUsingEmulator()) { - Backend.logLocation = "gallery"; - Intent intent = new Intent(MainActivity.this, Gallery.class); - startActivity(intent); - return; - } - new Thread(new Runnable() { @Override public void run() { - if (Backend.wifi.fujiConnectToCmd((ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE))) { - Backend.print(MyWiFiComm.failReason); - } else { + try { + Backend.fujiConnectToCmd(); Backend.print("Connected to the camera"); Backend.logLocation = "gallery"; Intent intent = new Intent(MainActivity.this, Gallery.class); startActivity(intent); + } catch (Exception e) { + Backend.print(e.getMessage()); } } }).start(); } - public static MainActivity getInstance() { - return instance; - } - - public void setErrorText(String arg) { + public void setLogText(String arg) { handler.post(new Runnable() { @Override public void run() { - TextView error_msg = findViewById(R.id.error_msg); - error_msg.setText(arg); + TextView tv = findViewById(R.id.error_msg); + if (tv == null) return; + tv.setText(arg); } }); } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return LibUI.handleOptions(item, false); + } + + @Override + public void onBackPressed() { + LibUI.handleBack(false); + } } diff --git a/app/src/main/java/dev/danielc/fujiapp/MyWiFiComm.java b/app/src/main/java/dev/danielc/fujiapp/MyWiFiComm.java deleted file mode 100644 index 5e07798..0000000 --- a/app/src/main/java/dev/danielc/fujiapp/MyWiFiComm.java +++ /dev/null @@ -1,145 +0,0 @@ -// Native Java interface for JNI to use sockets - socket() doesn't work for some reason (probably thread issues) -// Copyright 2023 Daniel C - https://github.com/petabyt/fujiapp -package dev.danielc.fujiapp; - -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkRequest; -import android.os.Build; -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.net.SocketTimeoutException; -import dev.petabyt.camlib.*; - -public class MyWiFiComm extends WiFiComm { - public boolean isUsingEmulator = false; - - public final String TAG = "MyWiFiComm"; - private Socket cmdSocket = null; - private InputStream cmdInputStream = null; - private OutputStream cmdOutputStream = null; - public boolean fujiConnectToCmd(ConnectivityManager m) { - Backend.print("Connecting..."); - cmdSocket = connectWiFiSocket(m, Backend.FUJI_IP, Backend.FUJI_CMD_PORT); - - if (cmdSocket == null) { - // Little slower, only for debug builds - if (BuildConfig.DEBUG) { - Log.d(TAG, "Trying emulator"); - cmdSocket = connectWiFiSocket(m, Backend.FUJI_EMU_IP, Backend.FUJI_CMD_PORT); - if (cmdSocket == null) { - return true; - } - - isUsingEmulator = true; - } else { - return true; - } - } - - try { - cmdSocket.setSoTimeout(2000); - cmdInputStream = cmdSocket.getInputStream(); - cmdOutputStream = cmdSocket.getOutputStream(); - killSwitch = false; - Log.d(TAG, "Successful connection established"); - } catch (Exception e) { - // TODO: Handle - return true; - } - - return false; - } - - private Socket eventSocket = null; - private Socket videoSocket = null; - public boolean fujiConnectEventAndVideo(ConnectivityManager m) { - String ip = Backend.FUJI_IP; - if (isUsingEmulator) ip = Backend.FUJI_EMU_IP; - eventSocket = connectWiFiSocket(m, ip, Backend.FUJI_EVENT_PORT); - if (eventSocket == null) { - Backend.print("Failed to connect to event socket: " + failReason); - return true; - } - - videoSocket = connectWiFiSocket(m, ip, Backend.FUJI_VIDEO_PORT); - if (videoSocket == null) { - Backend.print("Failed to connect to video socket: " + failReason); - return true; - } - - return false; - } - - public int cmdWrite(byte[] data) { - if (killSwitch) { - Log.d(TAG, "kill switch on, breaking request"); - return -1; - } - try { - cmdOutputStream.write(data); - cmdOutputStream.flush(); - return data.length; - } catch (IOException e) { - Backend.print("Write error: " + e.getMessage()); - return -1; - } - } - - public int cmdRead(byte[] buffer, int length) { - int read = 0; - while (true) { - try { - if (killSwitch) return -2; - - int rc = cmdInputStream.read(buffer, read, length - read); - if (rc < 0) return rc; - read += rc; - if (read == length) return read; - - // Post progress percentage to progressBar - if (!Viewer.inProgress) continue; - final int progress = (int)((double)read / (double)length * 100.0); - Viewer.handler.post(new Runnable() { - @Override - public void run() { - Viewer.progressBar.setProgress(progress); - } - }); - } catch (IOException e) { - Backend.print("Error reading " + length + " bytes: " + e.getMessage()); - return -1; - } - } - } - - public synchronized void close() { - killSwitch = true; - try { - // Suck the remaining bytes out of socket - byte[] remaining = new byte[100]; - cmdInputStream.read(remaining); - } catch (Exception e) { - // I don't care - } - - try { - if (cmdSocket != null) { - cmdSocket.close(); - } - if (cmdInputStream != null) { - cmdInputStream.close(); - } - if (cmdOutputStream != null) { - cmdOutputStream.close(); - } - } catch (IOException e) { - Backend.print("Socket close error: " + e.getMessage()); - } - } -} diff --git a/app/src/main/java/dev/danielc/fujiapp/Tester.java b/app/src/main/java/dev/danielc/fujiapp/Tester.java index cf5fc70..7dc9e12 100644 --- a/app/src/main/java/dev/danielc/fujiapp/Tester.java +++ b/app/src/main/java/dev/danielc/fujiapp/Tester.java @@ -4,9 +4,13 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; + +import android.content.ClipData; +import android.os.Environment; import android.util.Log; import android.content.Intent; import android.text.Html; +import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; import android.os.Looper; @@ -20,6 +24,9 @@ import android.os.Build; import java.net.Socket; +import android.content.ClipboardManager; + +import libui.LibU; public class Tester extends AppCompatActivity { private Handler handler; @@ -116,35 +123,33 @@ protected void onCreate(Bundle savedInstanceState) { Backend.cTesterInit(this); if (Backend.cRouteLogs(Backend.getLogPath()) == 0) { - log("Routing logs to " + Backend.getLogPath()); - } else { - fail("Couldn't route logs to " + Backend.getLogPath() + ", running test anyway"); + log("Routing logs to memory buffer."); } - //connectBluetooth(); - ConnectivityManager m = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); new Thread(new Runnable() { @Override public void run() { - if (!Backend.cIsUsingEmulator()) { - if (Backend.wifi.fujiConnectToCmd(m)) { - log(Backend.wifi.failReason); - fail("Failed to connect to port on WiFi"); - return; - } + try { + Backend.fujiConnectToCmd(); + } catch (Exception e) { + fail(e.toString()); + verboseLog = Backend.cEndLogs(); + return; } log("Established connection, starting test thread"); mainTest(m); - Backend.wifi.close(); - Backend.cEndLogs(); + Backend.cmdSocket.close(); + verboseLog = Backend.cEndLogs(); + log("Hit the copy button to share the verbose log with devs."); } }).start(); } + private String verboseLog = null; private String currentLogs = ""; public void log(String str) { Log.d("fujiapp-dbg-tester", str); @@ -171,11 +176,12 @@ public void mainTest(ConnectivityManager m) { rc = Backend.cFujiTestStartRemoteSockets(); if (rc != 0) return; - if (Backend.wifi.fujiConnectEventAndVideo(m)) { + try { + Backend.fujiConnectEventAndVideo(); + log("Accepted connection from event and video ports"); + } catch (Exception e) { fail("Failed to accept connections from event and video ports"); return; - } else { - log("Accepted connection from event and video ports"); } rc = Backend.cFujiEndRemoteMode(); @@ -196,12 +202,27 @@ public void mainTest(ConnectivityManager m) { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } else if (item.getTitle() == "copy") { + if (verboseLog != null) { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Fudge log", verboseLog); + clipboard.setPrimaryClip(clip); + } else { + LibU.toast(this, "Test not completed yet"); + } } return super.onOptionsItemSelected(item); } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuItem menuItem = menu.add(Menu.NONE, Menu.NONE, Menu.NONE, "copy"); + menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + menuItem.setIcon(R.drawable.baseline_content_copy_24); + return true; + } } diff --git a/app/src/main/java/dev/danielc/fujiapp/Viewer.java b/app/src/main/java/dev/danielc/fujiapp/Viewer.java index 4f871a6..7a42f15 100644 --- a/app/src/main/java/dev/danielc/fujiapp/Viewer.java +++ b/app/src/main/java/dev/danielc/fujiapp/Viewer.java @@ -50,8 +50,6 @@ public class Viewer extends AppCompatActivity { public static PopupWindow popupWindow = null; public static ProgressBar progressBar = null; - public static boolean inProgress = false; - public Bitmap bitmap = null; public String filename = null; public byte[] file = null; @@ -69,9 +67,8 @@ public ProgressBar downloadPopup(Activity activity) { return popupView.findViewById(R.id.progress_bar); } - public void createDir(String directoryPath) { + public static void createDir(String directoryPath) { File directory = new File(directoryPath); - Backend.print(directoryPath); if (!directory.exists()) { if (!directory.mkdirs()) { return; @@ -135,12 +132,14 @@ public void share(String filename, byte[] data) { } } + ActionBar actionBar; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_viewer); - ActionBar actionBar = getSupportActionBar(); + actionBar = getSupportActionBar(); actionBar.setDisplayHomeAsUpEnabled(true); StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); @@ -152,92 +151,92 @@ protected void onCreate(Bundle savedInstanceState) { handler = new Handler(Looper.getMainLooper()); - // Start the popup only when activity 'key' is finished (activity is built and ready to go) - ViewTreeObserver viewTreeObserver = getWindow().getDecorView().getViewTreeObserver(); - viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + // Wait until activity is loaded + handler.post(new Runnable() { @Override - public void onGlobalLayout() { - getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this); + public void run() { Viewer.progressBar = downloadPopup(Viewer.this); + new Thread(new Runnable() { + @Override + public void run() { + loadThumb(handle); + } + }).start(); } }); + } - Thread thread = new Thread(new Runnable() { - @Override - public void run() { - try { - inProgress = true; - Log.d(TAG, "Getting object info"); - JSONObject jsonObject = Backend.fujiGetUncompressedObjectInfo(handle); - - filename = jsonObject.getString("filename"); - int size = jsonObject.getInt("compressedSize"); - int imgX = jsonObject.getInt("imgWidth"); - int imgY = jsonObject.getInt("imgHeight"); - - if (filename.endsWith(".MOV")) { - inProgress = false; - toast("This is a MOV, not supported yet"); - return; - } + private void loadThumb(int handle) { + try { + Log.d(TAG, "Getting object info"); + JSONObject jsonObject = Backend.fujiGetUncompressedObjectInfo(handle); - handler.post(new Runnable() { - @SuppressLint({"SetTextI18n", "DefaultLocale"}) - @Override - public void run() { - actionBar.setTitle(filename); - TextView tv = findViewById(R.id.fileInfo); - tv.setText("File size: " + String.format("%.2f", size / 1024.0 / 1024.0) - + "MB\n" + "Dimensions: " + imgX + "x" + imgY); - } - }); - - file = Backend.cFujiGetFile(handle); - - if (file == null) { - // IO error in downloading - throw new Backend.PtpErr(Backend.PTP_IO_ERR); - } else if (file.length == 0) { - // Runtime error in downloading, no error yet - throw new Exception("Error downloading image"); - } + filename = jsonObject.getString("filename"); + int size = jsonObject.getInt("compressedSize"); + int imgX = jsonObject.getInt("imgWidth"); + int imgY = jsonObject.getInt("imgHeight"); - // Scale image to acceptable texture size - bitmap = BitmapFactory.decodeByteArray(file, 0, file.length); - if (bitmap.getWidth() > GL10.GL_MAX_TEXTURE_SIZE) { - float ratio = ((float) bitmap.getHeight()) / ((float) bitmap.getWidth()); - // Will result in ~11mb tex, can do 4096, but uses 40ish megs, sometimes Android compains about OOM - // Might be able to increase for newer Androids - Bitmap newBitmap = Bitmap.createScaledBitmap(bitmap, - (int)(2048), - (int)(2048 * ratio), - false - ); - bitmap.recycle(); - bitmap = newBitmap; - } + if (filename.endsWith(".MOV")) { + Backend.cSetProgressBar(null); + toast("This is a MOV, not supported yet"); + return; + } - inProgress = false; - - handler.post(new Runnable() { - @Override - public void run() { - Viewer.popupWindow.dismiss(); - - ZoomageView zoomageView = findViewById(R.id.zoom_view); - zoomageView.setImageBitmap(bitmap); - } - }); - } catch (Backend.PtpErr e) { - toast("Download IO Error: " + e.rc); - Backend.reportError(e.rc, "Download error"); - } catch (Exception e) { - toast("Download Error: " + e.toString()); - Backend.reportError(Backend.PTP_IO_ERR, "Download error: " + e.toString()); + handler.post(new Runnable() { + @SuppressLint({"SetTextI18n", "DefaultLocale"}) + @Override + public void run() { + actionBar.setTitle(filename); + TextView tv = findViewById(R.id.fileInfo); + tv.setText("File size: " + String.format("%.2f", size / 1024.0 / 1024.0) + + "MB\n" + "Dimensions: " + imgX + "x" + imgY); } + }); + + Backend.cSetProgressBar(Viewer.progressBar); + file = Backend.cFujiGetFile(handle); + + if (file == null) { + // IO error in downloading + throw new Backend.PtpErr(Backend.PTP_IO_ERR); + } else if (file.length == 0) { + // Runtime error in downloading, no error yet + throw new Exception("Error downloading image"); } - }); - thread.start(); + + // Scale image to acceptable texture size + bitmap = BitmapFactory.decodeByteArray(file, 0, file.length); + if (bitmap.getWidth() > GL10.GL_MAX_TEXTURE_SIZE) { + float ratio = ((float) bitmap.getHeight()) / ((float) bitmap.getWidth()); + // Will result in ~11mb tex, can do 4096, but uses 40ish megs, sometimes Android compains about OOM + // Might be able to increase for newer Androids + Bitmap newBitmap = Bitmap.createScaledBitmap(bitmap, + (int)(2048), + (int)(2048 * ratio), + false + ); + bitmap.recycle(); + bitmap = newBitmap; + } + + Backend.cSetProgressBar(null); + + handler.post(new Runnable() { + @Override + public void run() { + Viewer.popupWindow.dismiss(); + + ZoomageView zoomageView = findViewById(R.id.zoom_view); + zoomageView.setImageBitmap(bitmap); + } + }); + } catch (Backend.PtpErr e) { + toast("Download IO Error: " + e.rc); + Backend.reportError(e.rc, "Download error"); + } catch (Exception e) { + toast("Download Error: " + e.toString()); + Backend.reportError(Backend.PTP_IO_ERR, "Download error: " + e.toString()); + } } @Override diff --git a/app/src/main/java/dev/petabyt/camlib/WiFiComm.java b/app/src/main/java/dev/petabyt/camlib/WiFiComm.java deleted file mode 100644 index 3381479..0000000 --- a/app/src/main/java/dev/petabyt/camlib/WiFiComm.java +++ /dev/null @@ -1,181 +0,0 @@ -// Basic wifi-priority socket interface for camlib -// Copyright Daniel Cook - Apache License -package dev.petabyt.camlib; - -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.NetworkInfo; -import android.net.NetworkRequest; -import android.os.Build; -import android.util.Log; -import java.net.InetSocketAddress; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.net.SocketTimeoutException; - -public class WiFiComm { - public static final String TAG = "camlib"; - private Socket socket; - private InputStream inputStream; - private OutputStream outputStream; - - public static String failReason = null; - - public boolean killSwitch = true; - - static int currentNetworkCallbackDone = 0; - static Socket currentNetworkCallbackSocket = null; - - static Network wifiDevice = null; - - static Socket tryConnectToSocket(Network net, String ip, int port) { - failReason = "None yet"; - Socket sock; - try { - // Create and connect to socket - sock = new Socket(); - - // Bind socket to the network device we selected - net.bindSocket(sock); - - //sock.setKeepAlive(true); - sock.setTcpNoDelay(true); - sock.setReuseAddress(true); - - sock.connect(new InetSocketAddress(ip, port), 1000); - } catch (SocketTimeoutException e) { - failReason = "Connection timed out"; - Log.d(TAG, e.toString()); - currentNetworkCallbackDone = -1; - return null; - } catch (Exception e) { - failReason = "Failed to connect."; - Log.d(TAG, e.toString()); - currentNetworkCallbackDone = -1; - return null; - } - - return sock; - } - - public static void startNetworkListeners(ConnectivityManager connectivityManager) { - NetworkRequest.Builder requestBuilder = new NetworkRequest.Builder(); - requestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); - ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() { - @Override - public void onAvailable(Network network) { - Log.d(TAG, "Wifi network is available"); - wifiDevice = network; - } - @Override - public void onLost(Network network) { - Log.e(TAG, "Lost network\n"); - wifiDevice = null; - } - @Override - public void onUnavailable() { - Log.e(TAG, "Network unavailable\n"); - wifiDevice = null; - } - }; - - connectivityManager.requestNetwork(requestBuilder.build(), networkCallback); - } - - public static Socket connectWiFiSocket(ConnectivityManager connectivityManager, String ip, int port) { - currentNetworkCallbackSocket = null; - currentNetworkCallbackDone = 0; - - NetworkInfo wifiInfo = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - if (!wifiInfo.isAvailable()) { - failReason = "WiFi is not available."; - return null; - } else if (!wifiInfo.isConnected()) { - failReason = "Not connected to a WiFi network."; - return null; - } - - if (wifiDevice == null) { - failReason = "Not connected to WiFi."; - return null; - } - - return tryConnectToSocket(wifiDevice, ip, port); - } - - public boolean connect(String ipAddress, int port, int timeout) { - try { - socket = new Socket(); - socket.connect(new java.net.InetSocketAddress(ipAddress, port), timeout); - socket.setSoTimeout(timeout); - inputStream = socket.getInputStream(); - outputStream = socket.getOutputStream(); - killSwitch = false; - return false; - } catch (SocketTimeoutException e) { - failReason = "No connection found."; - } catch (IOException e) { - failReason = "Error connecting to the server: " + e.getMessage(); - } - return true; - } - - public int write(byte[] data, int length) { - if (killSwitch) return -1; - try { - outputStream.write(data); - outputStream.flush(); - return data.length; - } catch (IOException e) { - failReason = "Error writing to the server: " + e.getMessage(); - return -1; - } - } - - public int read(byte[] buffer, int length) { - int read = 0; - while (true) { - try { - if (killSwitch) return -1; - - int rc = inputStream.read(buffer, read, length - read); - if (rc == -1) return rc; - read += rc; - if (read == length) return read; - - // Post progress percentage to progressBar -// final int progress = (int)((double)read / (double)length * 100.0); -// if (Viewer.handler == null) continue; -// Viewer.handler.post(new Runnable() { -// @Override -// public void run() { -// Viewer.progressBar.setProgress(progress); -// } -// }); - } catch (IOException e) { - failReason = "Error reading " + length + " bytes: " + e.getMessage(); - return -1; - } - } - } - - public synchronized void close() { - killSwitch = true; - try { - if (inputStream != null) { - inputStream.close(); - } - if (outputStream != null) { - outputStream.close(); - } - if (socket != null) { - socket.close(); - } - } catch (IOException e) { - failReason = "Error closing the socket: " + e.getMessage(); - } - } -} diff --git a/app/src/main/java/libui/LibUI.java b/app/src/main/java/libui/LibUI.java new file mode 100644 index 0000000..d05f367 --- /dev/null +++ b/app/src/main/java/libui/LibUI.java @@ -0,0 +1,514 @@ +package libui; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; +import android.widget.PopupWindow; +import android.widget.RelativeLayout; +import android.widget.ScrollView; +import android.widget.Space; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Lifecycle; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; + +import java.util.ArrayList; + +public class LibUI { + public static Context ctx = null; + public static ActionBar actionBar = null; + + // uiWindow (popup) background drawable style resource + public static int popupDrawableResource = 0; + + // Background drawable resource for buttons + public static int buttonBackgroundResource = 0; + + public static Boolean useActionBar = true; + + public static void start(AppCompatActivity act) { + ctx = (Context)act; + waitUntilActivityLoaded(act); + } + + // Common way of telling when activity is done loading + public static void waitUntilActivityLoaded(Activity activity) { + ViewTreeObserver viewTreeObserver = activity.getWindow().getDecorView().getViewTreeObserver(); + viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + activity.getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this); + init(); + } + }); + } + + private static void init() { + if (useActionBar) { + actionBar = ((AppCompatActivity)ctx).getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + } + } + + public static void setTitle(String title) { + actionBar.setTitle(title); + } + + public static class MyFragment extends Fragment { + ViewGroup view; + MyFragment(ViewGroup v) { + view = v; + } + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return view; + } + } + + public static class MyFragmentStateAdapter extends FragmentStateAdapter { + private ArrayList arrayList = new ArrayList<>(); + public MyFragmentStateAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle) { + super(fragmentManager, lifecycle); + } + + public void addViewGroup(ViewGroup vg) { + arrayList.add(vg); + } + + @NonNull + @Override + public Fragment createFragment(int position) { + return new MyFragment(arrayList.get(position)); + } + + @Override + public int getItemCount() { + return arrayList.size(); + } + } + + public static class MyOnClickListener implements View.OnClickListener { + private long ptr; + private long arg1; + private long arg2; + public MyOnClickListener(long ptr, long arg1, long arg2) { + this.ptr = ptr; + this.arg1 = arg1; + this.arg2 = arg2; + } + + @Override + public void onClick(View v) { + LibUI.callFunction(ptr, arg1, arg2); + } + } + + public static View form(String name) { + LinearLayout layout = new LinearLayout(ctx); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + layout.setPadding(5, 5, 5, 5); + + TextView title = new TextView(ctx); + title.setPadding(5, 5, 5, 5); + title.setTypeface(Typeface.DEFAULT_BOLD); + title.setText(name); + title.setLayoutParams(new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT + )); + + layout.addView(title); + + return layout; + } + + public static void formAppend(View form, String name, View child) { + LinearLayout entry = new LinearLayout(ctx); + entry.setOrientation(LinearLayout.HORIZONTAL); + entry.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + entry.setPadding(5, 5, 5, 5); + + TextView entryName = new TextView(ctx); + entryName.setPadding(40, 20, 20, 20); + entryName.setText(name); + entryName.setLayoutParams(new LinearLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + )); + entry.addView(entryName); + + entry.addView(child); + + ((LinearLayout)form).addView(entry); + } + + public static View button(String text) { + Button b = new Button(ctx); + + if (buttonBackgroundResource != 0) { + b.setBackground(ContextCompat.getDrawable(ctx, buttonBackgroundResource)); + } + + b.setLayoutParams(new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT, + 1.0f + )); + + b.setTextSize(14f); + + b.setText(text); + return (View)b; + } + + public static View label(String text) { + TextView lbl = new TextView(ctx); + lbl.setText(text); + lbl.setTextSize(15f); + return (View)lbl; + } + + public static View tabLayout() { + LinearLayout layout = new LinearLayout(ctx); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + TabLayout tl = new TabLayout(ctx); + tl.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + ViewPager2 pager = new ViewPager2(ctx); + pager.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + MyFragmentStateAdapter frag = new MyFragmentStateAdapter( + ((AppCompatActivity)ctx).getSupportFragmentManager(), + ((AppCompatActivity)ctx).getLifecycle() + ); + pager.setAdapter(frag); + + tl.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + pager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + } + }); + + pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + tl.selectTab(tl.getTabAt(position)); + } + }); + + layout.addView(tl); + layout.addView(pager); + + return layout; + } + + private static void addTab(View parent, String name, View child) { + // TabLayout is child 0, add a new tab + TabLayout tl = (TabLayout)(((ViewGroup)parent).getChildAt(0)); + TabLayout.Tab tab = tl.newTab(); + tab.setText(name); + tl.addTab(tab); + + // ViewPager2 is the second child, we can get the custom fragment adapter from it + ViewPager2 vp = (ViewPager2)(((ViewGroup)parent).getChildAt(1)); + MyFragmentStateAdapter frag = (MyFragmentStateAdapter)vp.getAdapter(); + + ScrollView sv = new ScrollView(ctx); + sv.addView(child); + + frag.addViewGroup(sv); + } + + public static class Screen { + int displayOptions; + int id; + String title; + View content; + }; + + static ArrayList screens = new ArrayList(); + static Screen origActivity = new Screen(); + + public static void switchScreen(View view, String title) { + Boolean delay = true; + + if (delay) { + try { + Thread.sleep(100); + } catch (Exception e) {} + } + + ActionBar actionBar = ((AppCompatActivity)ctx).getSupportActionBar(); + + ScrollView layout = new ScrollView(ctx); + layout.addView(view); + + Screen screen = new Screen(); + screen.id = screens.size(); + screen.title = title; + screen.content = layout; + + if (screens.size() == 0) { + origActivity.content = ((ViewGroup)((Activity)ctx).findViewById(android.R.id.content)).getChildAt(0); + origActivity.title = (String)actionBar.getTitle(); + origActivity.displayOptions = actionBar.getDisplayOptions(); + } + + screens.add(screen); + + ((Activity)ctx).setContentView(layout); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setTitle(title); + } + + private static void screenGoBack() { + Screen screen = screens.remove(screens.size() - 1); + + if (screens.size() == 0) { + ((Activity)ctx).setContentView(origActivity.content); + + ActionBar actionBar = ((AppCompatActivity)ctx).getSupportActionBar(); + actionBar.setTitle(origActivity.title); + if ((origActivity.displayOptions & ActionBar.DISPLAY_SHOW_HOME) == 1) { + actionBar.setDisplayHomeAsUpEnabled(true); + } else { + actionBar.setDisplayHomeAsUpEnabled(false); + } + } else { + ((Activity)ctx).setContentView(screen.content); + + ActionBar actionBar = ((AppCompatActivity)ctx).getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setTitle(screen.title); + } + } + + public static boolean handleBack(boolean allowBack) { + if (screens.size() == 0) { + if (allowBack) { + ((Activity) ctx).finish(); + } + } else { + screenGoBack(); + } + return true; + } + + public static boolean handleOptions(MenuItem item, boolean allowBack) { + switch (item.getItemId()) { + case android.R.id.home: + handleBack(allowBack); + return true; + } + + return ((Activity)ctx).onOptionsItemSelected(item); + } + + public static class Popup { + PopupWindow popupWindow; + public void dismiss() { + this.popupWindow.dismiss(); + } + + String title; + + public void setChild(View v) { + LinearLayout rel = new LinearLayout(ctx); + + actionBar = ((AppCompatActivity)ctx).getSupportActionBar(); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setTitle(title); + + LinearLayout bar = new LinearLayout(ctx); + rel.setPadding(10, 10, 10, 10); + rel.setOrientation(LinearLayout.HORIZONTAL); + rel.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + Button back = new Button(ctx); + back.setText("Close"); + if (buttonBackgroundResource != 0) { + back.setBackground(ContextCompat.getDrawable(ctx, buttonBackgroundResource)); + } + + back.setTextSize(14f); + + back.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + Thread.sleep(100); + } catch (Exception e) {} + dismiss(); + } + }); + + bar.addView(back); + TextView tv = new TextView(ctx); + tv.setText(title); + tv.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + tv.setPadding(20, 0, 0, 0); + tv.setTextSize(20f); + tv.setGravity(Gravity.CENTER); + bar.addView(tv); + + rel.setOrientation(LinearLayout.VERTICAL); + if (popupDrawableResource != 0) { + rel.setBackground(ContextCompat.getDrawable(ctx, popupDrawableResource)); + } + rel.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + TypedValue typedValue = new TypedValue(); + if (ctx.getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true)) { + rel.setBackgroundColor(typedValue.data); + } + + rel.addView(bar); + + LinearLayout layout = new LinearLayout(ctx); + layout.setPadding(20, 20, 20, 20); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + + layout.addView(v); + rel.addView(layout); + + this.popupWindow.setContentView(rel); + this.popupWindow.showAtLocation(((Activity)ctx).getWindow().getDecorView().getRootView(), Gravity.CENTER, 0, 0); + } + + Popup(String title, int options) { + try { + Thread.sleep(100); + } catch (Exception e) {} + + DisplayMetrics displayMetrics = new DisplayMetrics(); + ((Activity)ctx).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + int height = displayMetrics.heightPixels; + int width = displayMetrics.widthPixels; + this.title = title; + + this.popupWindow = new PopupWindow( + (int)(width / 1.5), + (int)(height / 2.0) + ); + + this.popupWindow.setOutsideTouchable(false); + } + } + + private static LibUI.Popup openWindow(String title, int options) { + LibUI.Popup popup = new LibUI.Popup(title, options); + return popup; + } + + private static void setClickListener(View v, long ptr, long arg1, long arg2) { + v.setOnClickListener(new MyOnClickListener(ptr, arg1, arg2)); + } + + private static ViewGroup linearLayout(int orientation) { + LinearLayout layout = new LinearLayout(ctx); + layout.setOrientation(orientation); + layout.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + return (ViewGroup)layout; + } + + private static void setPadding(View v, int l, int t, int r, int b) { + v.setPadding(l, t, r, b); + } + + private static String getString(String name) { + Resources res = ctx.getResources(); + return res.getString(res.getIdentifier(name, "string", ctx.getPackageName())); + } + + private static int getView(String name) { + Resources res = ctx.getResources(); + return res.getIdentifier(name, "id", ctx.getPackageName()); + } + + private static void toast(String text) { + Toast.makeText(ctx, text, Toast.LENGTH_SHORT).show(); + } + + private static void runRunnable(long ptr, long arg1, long arg2) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + callFunction(ptr, arg1, arg2); + } + }); + } + + private static native void callFunction(long ptr, long arg1, long arg2); + + private int dpToPx(int dp) { + Resources r = ctx.getResources(); + return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics())); + } +} diff --git a/app/src/main/res/drawable/baseline_content_copy_24.xml b/app/src/main/res/drawable/baseline_content_copy_24.xml new file mode 100644 index 0000000..0fb13c2 --- /dev/null +++ b/app/src/main/res/drawable/baseline_content_copy_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/grey_button.xml b/app/src/main/res/drawable/grey_button.xml new file mode 100644 index 0000000..ee95204 --- /dev/null +++ b/app/src/main/res/drawable/grey_button.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 3470f29..b717ed0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -9,7 +9,7 @@ +