diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa724b7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..b589d56
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..b268ef3
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..32522c1
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/migrations.xml b/.idea/migrations.xml
new file mode 100644
index 0000000..f8051a6
--- /dev/null
+++ b/.idea/migrations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..0ad17cb
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..1337178
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,42 @@
+plugins {
+ alias(libs.plugins.android.application)
+}
+
+android {
+ namespace 'com.example.epaperuploader'
+ compileSdk 34
+
+ defaultConfig {
+ applicationId "com.example.epaperuploader"
+ minSdk 24
+ targetSdk 34
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+
+ implementation libs.appcompat
+ implementation libs.material
+ implementation libs.activity
+ implementation libs.constraintlayout
+ testImplementation libs.junit
+ androidTestImplementation libs.ext.junit
+ androidTestImplementation libs.espresso.core
+ implementation 'com.google.code.gson:gson:2.10.1'
+ implementation("com.vanniktech:android-image-cropper:4.6.0")
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/example/epaperuploader/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/epaperuploader/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..dacf005
--- /dev/null
+++ b/app/src/androidTest/java/com/example/epaperuploader/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.example.epaperuploader;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ assertEquals("com.example.epaperuploader", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5e60f97
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/example/epaperuploader/BLEDevice.java b/app/src/main/java/com/example/epaperuploader/BLEDevice.java
new file mode 100644
index 0000000..8e4c1d6
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/BLEDevice.java
@@ -0,0 +1,35 @@
+package com.example.epaperuploader;
+
+import android.bluetooth.BluetoothDevice;
+
+/**
+ * 作者:yeqianyun on 2019/11/6 17:22
+ * 邮箱:1612706976@qq.com
+ *
+ * BLE蓝牙设备
+ */
+public class BLEDevice {
+ private BluetoothDevice bluetoothDevice; //蓝牙设备
+ private int RSSI; //蓝牙信号
+
+ public BLEDevice(BluetoothDevice bluetoothDevice, int RSSI) {
+ this.bluetoothDevice = bluetoothDevice;
+ this.RSSI = RSSI;
+ }
+
+ public BluetoothDevice getBluetoothDevice() {
+ return bluetoothDevice;
+ }
+
+ public void setBluetoothDevice(BluetoothDevice bluetoothDevice) {
+ this.bluetoothDevice = bluetoothDevice;
+ }
+
+ public int getRSSI() {
+ return RSSI;
+ }
+
+ public void setRSSI(int RSSI) {
+ this.RSSI = RSSI;
+ }
+}
diff --git a/app/src/main/java/com/example/epaperuploader/BluetoothLeService.java b/app/src/main/java/com/example/epaperuploader/BluetoothLeService.java
new file mode 100644
index 0000000..03f68d0
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/BluetoothLeService.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.epaperuploader;
+
+import android.annotation.SuppressLint;
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Service for managing connection and data communication with a GATT server hosted on a
+ * given Bluetooth LE device.
+ */
+public class BluetoothLeService extends Service {
+ private final static String TAG = BluetoothLeService.class.getSimpleName();
+
+ private BluetoothManager mBluetoothManager;
+ private BluetoothAdapter mBluetoothAdapter;
+ private String mBluetoothDeviceAddress;
+ public static BluetoothGatt mBluetoothGatt;
+ private int mConnectionState = STATE_DISCONNECTED;
+
+ private static final int STATE_DISCONNECTED = 0;
+ private static final int STATE_CONNECTING = 1;
+ private static final int STATE_CONNECTED = 2;
+
+ public final static String ACTION_GATT_CONNECTED =
+ "ACTION_GATT_CONNECTED";
+ public final static String ACTION_GATT_DISCONNECTED =
+ "ACTION_GATT_DISCONNECTED";
+ public final static String ACTION_GATT_SERVICES_DISCOVERED =
+ "ACTION_GATT_SERVICES_DISCOVERED";
+ public final static String ACTION_DATA_AVAILABLE =
+ "ACTION_DATA_AVAILABLE";
+ public final static String EXTRA_DATA =
+ "EXTRA_DATA";
+
+ public final static UUID UUID_HEART_RATE_MEASUREMENT =
+ UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);
+
+ // Implements callback methods for GATT events that the app cares about. For example,
+ // connection change and services discovered.
+ private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
+ @SuppressLint("MissingPermission")
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ String intentAction;
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ intentAction = ACTION_GATT_CONNECTED;
+ mConnectionState = STATE_CONNECTED;
+ broadcastUpdate(intentAction);
+ Log.i(TAG, "Connected to GATT server.");
+ // Attempts to discover services after successful connection.
+ Log.i(TAG, "Attempting to start service discovery:" +
+ mBluetoothGatt.discoverServices());
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ gatt.readPhy();
+ }
+
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+ intentAction = ACTION_GATT_DISCONNECTED;
+ mConnectionState = STATE_DISCONNECTED;
+ Log.i(TAG, "Disconnected from GATT server.");
+ broadcastUpdate(intentAction);
+ }
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
+ } else {
+ Log.w(TAG, "onServicesDiscovered received: " + status);
+ }
+ }
+
+ @Override
+ public void onCharacteristicRead(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic,
+ int status) {
+ if (status == BluetoothGatt.GATT_SUCCESS) {
+ broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
+ }
+ }
+
+ @Override
+ public void onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
+ }
+
+ @Override
+ public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+ super.onCharacteristicWrite(gatt, characteristic, status);
+ MainActivity.handlerBt.handleUploadingStage();
+ }
+
+ @Override
+ public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
+ super.onPhyRead(gatt, txPhy, rxPhy, status);
+ Log.e(TAG, "onPhyRead: txPhy:" + txPhy + ", rxPhy:" + rxPhy);
+ gatt.setPreferredPhy(BluetoothDevice.PHY_LE_2M, BluetoothDevice.PHY_LE_2M,
+ BluetoothDevice.PHY_OPTION_NO_PREFERRED);
+ }
+
+ @Override
+ public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) {
+ super.onPhyUpdate(gatt, txPhy, rxPhy, status);
+ Log.e(TAG, "onPhyRead: txPhy:" + txPhy + ", rxPhy:" + rxPhy);
+ }
+ };
+
+ private void broadcastUpdate(final String action) {
+ final Intent intent = new Intent(action);
+ sendBroadcast(intent);
+ }
+
+ private void broadcastUpdate(final String action,
+ final BluetoothGattCharacteristic characteristic) {
+ final Intent intent = new Intent(action);
+
+ // This is special handling for the Heart Rate Measurement profile. Data parsing is
+ // carried out as per profile specifications:
+ // http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml
+ if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
+ int flag = characteristic.getProperties();
+ int format = -1;
+ if ((flag & 0x01) != 0) {
+ format = BluetoothGattCharacteristic.FORMAT_UINT16;
+ Log.d(TAG, "Heart rate format UINT16.");
+ } else {
+ format = BluetoothGattCharacteristic.FORMAT_UINT8;
+ Log.d(TAG, "Heart rate format UINT8.");
+ }
+ final int heartRate = characteristic.getIntValue(format, 1);
+ Log.d(TAG, String.format("Received heart rate: %d", heartRate));
+ intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
+ } else {
+ // For all other profiles, writes the data formatted in HEX.
+ final byte[] data = characteristic.getValue();
+ if (data != null && data.length > 0) {
+ final StringBuilder stringBuilder = new StringBuilder(data.length);
+ for(byte byteChar : data)
+ stringBuilder.append(String.format("%02X ", byteChar));
+ intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString());
+ }
+ }
+ sendBroadcast(intent);
+ }
+
+ public class LocalBinder extends Binder {
+ BluetoothLeService getService() {
+ return BluetoothLeService.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ Log.e(TAG, "onBind is calling:" + mBinder);
+ return mBinder;
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ // After using a given device, you should make sure that BluetoothGatt.close() is called
+ // such that resources are cleaned up properly. In this particular example, close() is
+ // invoked when the UI is disconnected from the Service.
+ close();
+ return super.onUnbind(intent);
+ }
+
+ private final IBinder mBinder = new LocalBinder();
+
+ /**
+ * Initializes a reference to the local Bluetooth adapter.
+ *
+ * @return Return true if the initialization is successful.
+ */
+ public boolean initialize() {
+ // For API level 18 and above, get a reference to BluetoothAdapter through
+ // BluetoothManager.
+ if (mBluetoothManager == null) {
+ mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
+ if (mBluetoothManager == null) {
+ Log.e(TAG, "Unable to initialize BluetoothManager.");
+ return false;
+ }
+ }
+
+ mBluetoothAdapter = mBluetoothManager.getAdapter();
+ if (mBluetoothAdapter == null) {
+ Log.e(TAG, "Unable to obtain a BluetoothAdapter.");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Connects to the GATT server hosted on the Bluetooth LE device.
+ *
+ * @param address The device address of the destination device.
+ *
+ * @return Return true if the connection is initiated successfully. The connection result
+ * is reported asynchronously through the
+ * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
+ * callback.
+ */
+ @SuppressLint("MissingPermission")
+ public boolean connect(final String address) {
+ if (mBluetoothAdapter == null || address == null) {
+ Log.w(TAG, "BluetoothAdapter not initialized or unspecified address.");
+ return false;
+ }
+
+ // Previously connected device. Try to reconnect.
+ if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
+ && mBluetoothGatt != null) {
+ Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
+ if (mBluetoothGatt.connect()) {
+ mConnectionState = STATE_CONNECTING;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
+ if (device == null) {
+ Log.w(TAG, "Device not found. Unable to connect.");
+ return false;
+ }
+ // We want to directly connect to the device, so we are setting the autoConnect
+ // parameter to false.
+ mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
+ mBluetoothGatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
+ Log.d(TAG, "Trying to create a new connection.");
+ mBluetoothDeviceAddress = address;
+ mConnectionState = STATE_CONNECTING;
+ return true;
+ }
+
+ /**
+ * Disconnects an existing connection or cancel a pending connection. The disconnection result
+ * is reported asynchronously through the
+ * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)}
+ * callback.
+ */
+ @SuppressLint("MissingPermission")
+ public void disconnect() {
+ if (mBluetoothAdapter == null || mBluetoothGatt == null) {
+ Log.w(TAG, "BluetoothAdapter not initialized");
+ return;
+ }
+ mBluetoothGatt.disconnect();
+ }
+
+ /**
+ * After using a given BLE device, the app must call this method to ensure resources are
+ * released properly.
+ */
+ @SuppressLint("MissingPermission")
+ public void close() {
+ if (mBluetoothGatt == null) {
+ return;
+ }
+ mBluetoothGatt.close();
+ mBluetoothGatt = null;
+ }
+
+ /**
+ * Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported
+ * asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)}
+ * callback.
+ *
+ * @param characteristic The characteristic to read from.
+ */
+ @SuppressLint("MissingPermission")
+ public void readCharacteristic(BluetoothGattCharacteristic characteristic) {
+ if (mBluetoothAdapter == null || mBluetoothGatt == null) {
+ Log.w(TAG, "BluetoothAdapter not initialized");
+ return;
+ }
+ mBluetoothGatt.readCharacteristic(characteristic);
+ }
+
+ @SuppressLint("MissingPermission")
+ public void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
+ if (mBluetoothAdapter == null || mBluetoothGatt == null) {
+ Log.w(TAG, "BluetoothAdapter not initialized");
+ return;
+ }
+ mBluetoothGatt.writeCharacteristic(characteristic);
+ }
+
+ /**
+ * Enables or disables notification on a give characteristic.
+ *
+ * @param characteristic Characteristic to act on.
+ * @param enabled If true, enable notification. False otherwise.
+ */
+ @SuppressLint("MissingPermission")
+ public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic,
+ boolean enabled) {
+ if (mBluetoothAdapter == null || mBluetoothGatt == null) {
+ Log.w(TAG, "BluetoothAdapter not initialized");
+ return;
+ }
+ mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
+
+ // This is specific to Heart Rate Measurement.
+ if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
+ BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
+ UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
+ descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
+ mBluetoothGatt.writeDescriptor(descriptor);
+ }
+ }
+
+ /**
+ * Retrieves a list of supported GATT services on the connected device. This should be
+ * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully.
+ *
+ * @return A {@code List} of supported services.
+ */
+ public List getSupportedGattServices() {
+ if (mBluetoothGatt == null) return null;
+
+ return mBluetoothGatt.getServices();
+ }
+}
diff --git a/app/src/main/java/com/example/epaperuploader/CropImageActivity.java b/app/src/main/java/com/example/epaperuploader/CropImageActivity.java
new file mode 100644
index 0000000..be7fa40
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/CropImageActivity.java
@@ -0,0 +1,91 @@
+package com.example.epaperuploader;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.canhub.cropper.CropImageView;
+import com.example.epaperuploader.image_processing.EPaperDisplay;
+
+import java.io.File;
+import java.io.IOException;
+
+public class CropImageActivity extends AppCompatActivity {
+ private Uri sourceUri;
+ private Uri saveUri;
+ private CropImageView vCropImageView;
+ private final ActivityResultLauncher activityResultLauncher = registerForActivityResult(
+ new ActivityResultContracts.GetContent(),
+ uri -> vCropImageView.setImageUriAsync(uri));
+
+ private Button btnRotation;
+ private Button btnCrop;
+
+ private int width = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width;
+ private int height = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_crop_image);
+
+ vCropImageView = findViewById(R.id.cropImageView);
+ vCropImageView.setOnSetImageUriCompleteListener(new CropImageView.OnSetImageUriCompleteListener() {
+ @Override
+ public void onSetImageUriComplete(@NonNull CropImageView cropImageView, @NonNull Uri uri, @Nullable Exception e) {
+ sourceUri = uri;
+ vCropImageView.setAspectRatio(width, height);
+ }
+ });
+
+ vCropImageView.setOnCropImageCompleteListener(new CropImageView.OnCropImageCompleteListener() {
+ @Override
+ public void onCropImageComplete(@NonNull CropImageView cropImageView, @NonNull CropImageView.CropResult cropResult) {
+ Intent intent = new Intent();
+ intent.putExtra("FILE_NAME", saveUri);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+ });
+
+ btnRotation = findViewById(R.id.btnRotation);
+ btnRotation.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ vCropImageView.rotateImage(90);
+ vCropImageView.setAspectRatio(width, height);
+ }
+ });
+
+ btnCrop = findViewById(R.id.btnCrop);
+ btnCrop.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ saveUri = Uri.fromFile(File.createTempFile("esp32_image_crop", "png"));
+ vCropImageView.croppedImageAsync(
+ Bitmap.CompressFormat.PNG,
+ 30,
+ width,
+ height,
+ CropImageView.RequestSizeOptions.NONE,
+ saveUri
+ );
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+
+ activityResultLauncher.launch("image/*");
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/epaperuploader/DeviceScanActivity.java b/app/src/main/java/com/example/epaperuploader/DeviceScanActivity.java
new file mode 100644
index 0000000..506672f
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/DeviceScanActivity.java
@@ -0,0 +1,344 @@
+package com.example.epaperuploader;
+
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Activity for scanning and displaying available Bluetooth LE devices.
+ */
+//public class DeviceScanActivity extends ListActivity {
+public class DeviceScanActivity extends AppCompatActivity {
+ private LeDeviceListAdapter mLeDeviceListAdapter;
+ private BluetoothAdapter mBluetoothAdapter;
+ private boolean mScanning;
+ private Handler mHandler;
+
+ private ListView deviceList;
+
+ private static final int REQUEST_ENABLE_BT = 1;
+ // Stops scanning after 10 seconds.
+ private static final long SCAN_PERIOD = 10000;
+
+ /**
+ * This block is for requesting permissions up to Android 12+
+ *
+ */
+
+ private static final int PERMISSIONS_REQUEST_CODE = 191;
+ private static final String[] BLE_PERMISSIONS = new String[]{
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ };
+ @SuppressLint("InlinedApi")
+ private static final String[] ANDROID_12_BLE_PERMISSIONS = new String[]{
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_ADVERTISE,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ };
+
+ public static void requestBlePermissions(Activity activity, int requestCode) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ ActivityCompat.requestPermissions(activity, ANDROID_12_BLE_PERMISSIONS, requestCode);
+ else
+ ActivityCompat.requestPermissions(activity, BLE_PERMISSIONS, requestCode);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_device_scan);
+ getSupportActionBar().setTitle(R.string.title_devices);
+ mHandler = new Handler();
+
+ requestBlePermissions(this, PERMISSIONS_REQUEST_CODE);
+
+ // Use this check to determine whether BLE is supported on the device. Then you can
+ // selectively disable BLE-related features.
+ if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
+ Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+ // Initializes a Bluetooth adapter. For API level 18 and above, get a reference to
+ // BluetoothAdapter through BluetoothManager.
+ final BluetoothManager bluetoothManager =
+ (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
+ mBluetoothAdapter = bluetoothManager.getAdapter();
+
+ // Checks if Bluetooth is supported on the device.
+ if (mBluetoothAdapter == null) {
+ Toast.makeText(this, R.string.error_bluetooth_not_supported, Toast.LENGTH_SHORT).show();
+ finish();
+ return;
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.main, menu);
+ if (!mScanning) {
+ menu.findItem(R.id.menu_stop).setVisible(false);
+ menu.findItem(R.id.menu_scan).setVisible(true);
+ } else {
+ menu.findItem(R.id.menu_stop).setVisible(true);
+ menu.findItem(R.id.menu_scan).setVisible(false);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int itemId = item.getItemId();
+ if (itemId == R.id.menu_scan) {
+ mLeDeviceListAdapter.clear();
+ scanLeDevice(true);
+ } else if (itemId == R.id.menu_stop) {
+ scanLeDevice(false);
+ }
+
+ return true;
+ }
+
+ @SuppressLint("MissingPermission")
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // Ensures Bluetooth is enabled on the device. If Bluetooth is not currently enabled,
+ // fire an intent to display a dialog asking the user to grant permission to enable it.
+ if (!mBluetoothAdapter.isEnabled()) {
+ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
+ }
+
+ // Initializes list view adapter.
+ mLeDeviceListAdapter = new LeDeviceListAdapter();
+ deviceList = findViewById(R.id.device_list);
+ deviceList.setAdapter(mLeDeviceListAdapter);
+ deviceList.setOnItemClickListener(new OnDevListItemClick());
+ scanLeDevice(true);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // User chose not to enable Bluetooth.
+ if (requestCode == REQUEST_ENABLE_BT && resultCode == Activity.RESULT_CANCELED) {
+ finish();
+ return;
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ scanLeDevice(false);
+ mLeDeviceListAdapter.clear();
+ }
+
+ @SuppressLint("MissingPermission")
+ private class OnDevListItemClick implements AdapterView.OnItemClickListener
+ {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id)
+ {
+ // 停止扫描
+ if (mScanning) {
+ mBluetoothAdapter.stopLeScan(mLeScanCallback);
+ mScanning = false;
+ }
+ final BLEDevice device = mLeDeviceListAdapter.getDevice(position);
+ if (device == null) return;
+ Intent intent = new Intent();
+ intent.putExtra("DEVICE", device.getBluetoothDevice());
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private void scanLeDevice(final boolean enable) {
+ if (enable) {
+ // Stops scanning after a pre-defined scan period.
+ mHandler.postDelayed(new Runnable() {
+ @SuppressLint("MissingPermission")
+ @Override
+ public void run() {
+ mScanning = false;
+ mBluetoothAdapter.stopLeScan(mLeScanCallback);
+ invalidateOptionsMenu();
+ }
+ }, SCAN_PERIOD);
+
+ mScanning = true;
+ mBluetoothAdapter.startLeScan(mLeScanCallback);
+ } else {
+ mScanning = false;
+ mBluetoothAdapter.stopLeScan(mLeScanCallback);
+ }
+ invalidateOptionsMenu();
+ }
+
+ // Adapter for holding devices found through scanning.
+ private class LeDeviceListAdapter extends BaseAdapter {
+ private ArrayList mLeDevices;
+ private LayoutInflater mInflator;
+
+ public LeDeviceListAdapter() {
+ super();
+ mLeDevices = new ArrayList();
+ mInflator = DeviceScanActivity.this.getLayoutInflater();
+ }
+
+ public boolean isRepeat(List rawList, BLEDevice bleDevice) {
+ int Num = rawList.size();
+ if(Num==0)
+ {
+ return false;
+ }
+ String devName = bleDevice.getBluetoothDevice().getName();
+ String devAddress = bleDevice.getBluetoothDevice().getAddress();
+ for(int i =0;i 0)
+ viewHolder.deviceName.setText(deviceName);
+ else
+ viewHolder.deviceName.setText(R.string.unknown_device);
+ viewHolder.deviceAddress.setText(device.getBluetoothDevice().getAddress());
+ viewHolder.deviceRSSI.setText("RSSI: " + device.getRSSI());
+
+ return view;
+ }
+ }
+
+ // Device scan callback.
+ private BluetoothAdapter.LeScanCallback mLeScanCallback =
+ new BluetoothAdapter.LeScanCallback() {
+
+ @Override
+ public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ String devName = device.getName();
+ if (devName != null && devName.length() > 0) {
+ BLEDevice bleDevice = new BLEDevice(device, rssi);
+ // mLeDeviceListAdapter.addDevice(device);
+ mLeDeviceListAdapter.addDevice(bleDevice);
+ mLeDeviceListAdapter.notifyDataSetChanged();
+ }
+ }
+ });
+ }
+ };
+
+ static class ViewHolder {
+ TextView deviceName;
+ TextView deviceAddress;
+ TextView deviceRSSI;
+ }
+}
diff --git a/app/src/main/java/com/example/epaperuploader/DisplaysActivity.java b/app/src/main/java/com/example/epaperuploader/DisplaysActivity.java
new file mode 100644
index 0000000..e1451b6
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/DisplaysActivity.java
@@ -0,0 +1,71 @@
+package com.example.epaperuploader;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.epaperuploader.image_processing.EPaperDisplay;
+
+public class DisplaysActivity extends AppCompatActivity implements AdapterView.OnItemClickListener
+{
+ // View of displays list
+ //---------------------------------------------------------
+ private ListView listView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_displays);
+ getSupportActionBar().setTitle(R.string.disp);
+
+ // ListView, its adapter and listener
+ //--------------------------------------
+ listView = findViewById(R.id.displays_list);
+ listView.setAdapter(new DisplayListAdapter(this, EPaperDisplay.getDisplays()));
+ listView.setOnItemClickListener(this);
+ }
+
+ public void onCancel(View view) {
+ onBackPressed();
+ }
+
+ @Override
+ public void onBackPressed() {
+ setResult(RESULT_CANCELED, new Intent());
+ finish();
+ }
+
+ @Override
+ public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
+ EPaperDisplay.epdInd = i;
+ setResult(RESULT_OK, new Intent());
+ finish();
+ }
+
+
+ //---------------------------------------------------------
+ // File list adapter
+ //---------------------------------------------------------
+ private class DisplayListAdapter extends ArrayAdapter {
+ public DisplayListAdapter(Context context, EPaperDisplay[] array) {
+ // Standard one-line item layout
+ //-------------------------------------------------
+ super(context, android.R.layout.simple_list_item_1, array);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView view = (TextView) super.getView(position, convertView, parent);
+ if (view != null) view.setText(EPaperDisplay.getDisplays()[position].title);
+ return view;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/epaperuploader/FilteringActivity.java b/app/src/main/java/com/example/epaperuploader/FilteringActivity.java
new file mode 100644
index 0000000..2536103
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/FilteringActivity.java
@@ -0,0 +1,259 @@
+package com.example.epaperuploader;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.epaperuploader.image_processing.EPaperDisplay;
+import com.example.epaperuploader.image_processing.EPaperPicture;
+
+
+/**
+ *
Filtering activity
+ * The activity offers to select one of available image filters,
+ * which converts the loaded image for better pixel format
+ * converting required for selected display.
+ *
+ * @author Waveshare team
+ * @version 1.0
+ * @since 8/18/2018
+ */
+
+
+public class FilteringActivity extends AppCompatActivity
+{
+ // View
+ //------------------------------------------
+ private Button button;
+ private TextView textView;
+ private ImageView imageView;
+
+ private Button btnLevelMono;
+ private Button btnLevelColor;
+ private Button btnDitherMono;
+ private Button btnDitherMono2;
+ private Button btnDitherColor;
+ private Button btnDitherColor2;
+
+ private ImageView imgLevelMono;
+ private ImageView imgLevelColor;
+ private ImageView imgDitherMono;
+ private ImageView imgDitherMono2;
+ private ImageView imgDitherColor;
+ private ImageView imgDitherColor2;
+
+ private final static String TAG = FilteringActivity.class.getSimpleName();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_filtering);
+ getSupportActionBar().setTitle(R.string.filt);
+
+ // View
+ //------------------------------------------
+ textView = findViewById(R.id.txt_indexed);
+ imageView = findViewById(R.id.img_indexed);
+
+ btnLevelMono = findViewById(R.id.btn_wb_l);
+ btnLevelColor = findViewById(R.id.btn_wbrl);
+ btnDitherMono = findViewById(R.id.btn_wb_d);
+ btnDitherMono2 = findViewById(R.id.btn_wb_d2);
+ btnDitherColor = findViewById(R.id.btn_wbrd);
+ btnDitherColor2 = findViewById(R.id.btn_wbrd2);
+
+ imgLevelMono = findViewById(R.id.img_level_mono);
+ imgLevelColor = findViewById(R.id.img_level_color);
+ imgDitherMono = findViewById(R.id.img_dither_mono);
+ imgDitherMono2 = findViewById(R.id.img_dither_mono2);
+ imgDitherColor = findViewById(R.id.img_dither_color);
+ imgDitherColor2 = findViewById(R.id.img_dither_color2);
+
+ // Disable unavailable palettes
+ //------------------------------------------
+ boolean redIsEnabled = (EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].index & 1) != 0;
+
+ btnLevelColor.setEnabled(redIsEnabled);
+ btnDitherColor.setEnabled(redIsEnabled);
+ btnDitherColor2.setEnabled(redIsEnabled);
+
+ // Disable unavailable palettes just for 5.65f e-Paper
+ //------------------------------------------
+ if(EPaperDisplay.epdInd == 25 || EPaperDisplay.epdInd == 37 || EPaperDisplay.epdInd == 43 || EPaperDisplay.epdInd == 47)
+ {
+ redIsEnabled = false;
+ btnLevelMono.setEnabled(redIsEnabled);
+ btnDitherMono.setEnabled(redIsEnabled);
+ btnDitherMono2.setEnabled(redIsEnabled);
+ }
+
+ // 通过模拟点击,来实现预览
+ if (btnLevelMono.isEnabled()) {
+ btnLevelMono.callOnClick();
+ }
+ if (btnLevelColor.isEnabled()) {
+ btnLevelColor.callOnClick();
+ }
+ if (btnDitherMono.isEnabled()) {
+ btnDitherMono.callOnClick();
+ }
+ if (btnDitherMono2.isEnabled()) {
+ btnDitherMono2.callOnClick();
+ }
+ if (btnDitherColor.isEnabled()) {
+ btnDitherColor.callOnClick();
+ }
+
+ if (btnDitherColor2.isEnabled()) {
+ btnDitherColor2.callOnClick();
+ }
+ }
+
+ public void onImgClick(View view) {
+ int img_id = view.getId();
+ String name = "";
+ Button btn;
+ if (img_id == R.id.img_level_mono) {
+ btn = btnLevelMono;
+ } else if (img_id == R.id.img_level_color) {
+ btn = btnLevelColor;
+ } else if (img_id == R.id.img_dither_mono) {
+ btn = btnDitherMono;
+ } else if (img_id == R.id.img_dither_mono2) {
+ btn = btnDitherMono2;
+ } else if (img_id == R.id.img_dither_color) {
+ btn = btnDitherColor;
+ } else if (img_id == R.id.img_dither_color2) {
+ btn = btnDitherColor2;
+ } else {
+ return;
+ }
+
+ name = btn.getText().toString();
+
+ Log.e("TAG", "Image id: " + img_id + ", Name: " + name);
+ Intent intent = new Intent();
+ intent.putExtra("NAME", name);
+
+ btn.callOnClick();
+
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ // Accept the selected
+ public void onOk(View view)
+ {
+ // If palette is not selected, then exit
+ //-----------------------------------------------------
+ if (button == null) return;
+
+ // Close palette activity and return palette's name
+ //-----------------------------------------------------
+ Intent intent = new Intent();
+ intent.putExtra("NAME", button.getText().toString());
+
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ public void onCancel(View view)
+ {
+ onBackPressed();
+ }
+
+ @Override
+ public void onBackPressed()
+ {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+
+ public void onButtonClick(View view) {
+ int btnId = view.getId();
+ boolean isLvl;
+ boolean isRed;
+ boolean isEnhance = false;
+ if (btnId == R.id.btn_wb_l) {
+ isLvl = true;
+ isRed = false;
+ } else if (btnId == R.id.btn_wbrl) {
+ isLvl = true;
+ isRed = true;
+ } else if (btnId == R.id.btn_wb_d) {
+ isLvl = false;
+ isRed = false;
+ } else if (btnId == R.id.btn_wb_d2) {
+ isLvl = false;
+ isRed = false;
+ isEnhance = true;
+ } else if (btnId == R.id.btn_wbrd) {
+ isLvl = false;
+ isRed = true;
+ } else if (btnId == R.id.btn_wbrd2) {
+ isLvl = false;
+ isRed = true;
+ isEnhance = true;
+ } else {
+ Log.e(TAG, "Error id: " + btnId + ", name: " + view.getTransitionName());
+ return;
+ }
+ button = (Button)view;
+ run(isLvl, isRed, isEnhance);
+ }
+
+ public void run(boolean isLvl, boolean isRed, boolean isEnhance)
+ {
+ // Image processing
+ //-----------------------------------------------------
+ MainActivity.indTableImage = EPaperPicture.createIndexedImage(isLvl, isRed, isEnhance);
+
+ // Image view size calculation
+ //-----------------------------------------------------
+ int size = textView.getWidth();
+
+ imageView.setMaxHeight(Math.min(300, size));
+ imageView.setMinimumHeight(Math.min(200, size / 2));
+ imageView.setImageBitmap(MainActivity.indTableImage);
+
+ ImageView sampleImageView;
+ if (isLvl) {
+ if (isRed) {
+ sampleImageView = imgLevelColor;
+ } else {
+ sampleImageView = imgLevelMono;
+ }
+ } else {
+ if (isRed) {
+ if (isEnhance) {
+ sampleImageView = imgDitherColor2;
+ } else {
+ sampleImageView = imgDitherColor;
+ }
+ } else {
+ if (isEnhance) {
+ sampleImageView = imgDitherMono2;
+ } else {
+ sampleImageView = imgDitherMono;
+ }
+ }
+ }
+
+ sampleImageView.setMaxHeight(Math.min(300, size));
+ sampleImageView.setMinimumHeight(Math.min(200, size / 2));
+ sampleImageView.setImageBitmap(MainActivity.indTableImage);
+
+ // Show selected image filter
+ //-----------------------------------------------------
+ textView.setText(button.getText());
+ }
+}
diff --git a/app/src/main/java/com/example/epaperuploader/MainActivity.java b/app/src/main/java/com/example/epaperuploader/MainActivity.java
new file mode 100644
index 0000000..475ad83
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/MainActivity.java
@@ -0,0 +1,859 @@
+package com.example.epaperuploader;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import com.example.epaperuploader.communication.PermissionHelper;
+import com.example.epaperuploader.image_processing.EPaperDisplay;
+import com.google.gson.Gson;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public class MainActivity extends AppCompatActivity {
+ // Request codes
+ //-----------------------------
+ public static final int REQ_BLUETOOTH_CONNECTION = 2;
+ public static final int REQ_OPEN_FILE = 3;
+ public static final int REQ_DISPLAY_SELECTION = 4;
+ public static final int REQ_PALETTE_SELECTION = 5;
+ public static final int REQ_UPLOADING = 6;
+ public static final int SPI_PIN_CONFIG = 7;
+
+ private final static String TAG = MainActivity.class.getSimpleName();
+
+ // Image file name and path
+ //-----------------------------
+ public static String fileName;
+ public static String filePath;
+
+ // Data
+ //-----------------------------
+ public static Bitmap originalImage; // Loaded image with original pixel format
+ public static Bitmap indTableImage; // Filtered image with indexed colors
+
+ // Views
+ //-----------------------------
+ public TextView mIndexSpiConfig;
+ public TextView textBlue;
+ public TextView textLoad;
+ public TextView textDisp;
+ public TextView textFilt;
+ public Button button_file;
+
+ public ImageView pictFile; // View of loaded image
+ public ImageView pictFilt; // View of filtered image
+
+ private CheckBox cbIsCompress;
+ private static TextView tvSendInfo;
+ private static Button btnUploadBt;
+ private Button btnUploadWifi;
+ private static Button btnLoadDefault;
+ private static ProgressBar pbUploadProgress;
+
+ private static TextView tvUploadRatio;
+ private TextView tvDevAddress;
+ private static TextView tvConnectionState;
+ private static TextView tvIpAddress;
+ private static TextView tvHostName;
+
+ // Device
+ //-----------------------------
+ public static BluetoothDevice btDevice = null;
+
+ public static SocketHandlerBt handlerBt = null;
+ public SocketHandlerWifi handlerWifi = null;
+
+ // service
+ private static BluetoothLeService mBluetoothLeService = null;
+
+ private ArrayList> mGattCharacteristics =
+ new ArrayList>();
+
+ private BluetoothGattCharacteristic mCharacterRead;
+ private BluetoothGattCharacteristic mCharacterWriteMsg;
+ private static BluetoothGattCharacteristic mCharacterWriteData;
+
+ private static boolean mConnected = false;
+ private String mDeviceAddress;
+
+ private String mEsp32IpAddress = null;
+ private String mEsp32HostName = null;
+
+ // other
+ SharedPreferences sharedPreferences = null;
+ SharedPreferences.Editor config_editor;
+
+ public static int din_pin = 14;
+ public static int sck_pin = 13;
+ public static int cs_pin = 15;
+ public static int dc_pin = 27;
+ public static int rst_pin = 26;
+ public static int busy_pin = 25;
+
+ public static Context context;
+
+ public static int UPDATE_PROGESS = 1;
+ public static int UPDATE_SEND_INFO = 2;
+ public static int CONNECT_SUCCESS = 3;
+ public static int CONNECT_FAILURE = 4;
+ public static int DISCONNECT_SUCCESS = 5;
+ public static int SEND_SUCCESS = 6;
+ public static int SEND_FAILURE = 7;
+ public static int RECEIVE_SUCCESS = 8;
+ public static int RECEIVE_FAILURE = 9;
+ public static int UPDATE_WIFI_PARAMS = 10;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ // Image file name (null by default)
+ //-----------------------------------------------------
+ fileName = null;
+
+ // Image file path (external storage root by default)
+ //-----------------------------------------------------
+ filePath = Environment.getExternalStorageDirectory().getAbsolutePath();
+
+ mIndexSpiConfig = findViewById(R.id.index_spi_config);
+
+ sharedPreferences = getSharedPreferences("config", Context.MODE_PRIVATE);
+ config_editor = sharedPreferences.edit();
+ reload_config();
+
+ textBlue = findViewById(R.id.text_blue);
+ textLoad = findViewById(R.id.text_file);
+ textDisp = findViewById(R.id.text_disp);
+ textFilt = findViewById(R.id.text_filt);
+
+ pictFile = findViewById(R.id.pict_file);
+ pictFilt = findViewById(R.id.pict_filt);
+ button_file = findViewById(R.id.Button_file);
+
+ context = this;
+ // Data
+ //-----------------------------
+ originalImage = null;
+ indTableImage = null;
+ button_file.setEnabled(false);
+
+ // View
+ tvDevAddress = findViewById(R.id.device_address);
+ // connect state
+ tvConnectionState = findViewById(R.id.connect_state);
+ // ip address
+ tvIpAddress = findViewById(R.id.ip_address);
+ // ip address
+ tvHostName = findViewById(R.id.host_name);
+
+ cbIsCompress = findViewById(R.id.is_compress);
+ tvSendInfo = findViewById(R.id.send_info);
+ btnUploadBt = findViewById(R.id.btn_upload_bt);
+ btnUploadWifi = findViewById(R.id.btn_upload_wifi);
+ btnLoadDefault = findViewById(R.id.btn_load_default);
+
+ pbUploadProgress = findViewById(R.id.upload_progress);
+
+ //--------------------------------------
+ tvUploadRatio = findViewById(R.id.upload_text);
+ tvUploadRatio.setText("0%");
+
+ // Use this check to determine whether BLE is supported on the device. Then you can
+ // selectively disable BLE-related features.
+ if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
+ Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED ||
+ ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED ||
+ ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT");
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_ADVERTISE, Manifest.permission.BLUETOOTH_CONNECT},
+ 0x3);
+ }
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "ACCESS_FINE_LOCATION");
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
+ 0x9);
+ }
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "BLUETOOTH_ADMIN");
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.BLUETOOTH_ADMIN},
+ 0x6);
+ }
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.e(TAG, "WRITE_EXTERNAL_STORAGE");
+ //申请WRITE_EXTERNAL_STORAGE权限
+ ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ 0x3);
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter(), RECEIVER_EXPORTED);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater menuInflater = getMenuInflater();
+ menuInflater.inflate(R.menu.config_menu, menu);
+
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ if (menu != null) {
+ if (menu.getClass().getSimpleName().equalsIgnoreCase("MenuBuilder")) {
+ try {
+ Method method = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
+ method.setAccessible(true);
+ method.invoke(menu, true);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ return super.onMenuOpened(featureId, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int item_id = item.getItemId();
+
+ if (item_id == R.id.spi_pin_config) {
+ startActivityForResult(
+ new Intent(this, SpiPinConfigActivity.class),
+ SPI_PIN_CONFIG);
+ } else if (item_id == R.id.network_config) {
+ Toast.makeText(this, "偷懒,没做^_^", Toast.LENGTH_SHORT).show();
+ } else if (item_id == R.id.save_image) {
+ saveImage();
+ } else if (item_id == R.id.reboot_device) {
+ rebootDevice();
+ } else {
+ Toast.makeText(this, "unknown option:" + item_id, Toast.LENGTH_SHORT).show();
+ return false;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ // Handles various events fired by the Service.
+ // ACTION_GATT_CONNECTED: connected to a GATT server.
+ // ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
+ // ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
+ // ACTION_DATA_AVAILABLE: received data from the device. This can be a result of read
+ // or notification operations.
+ private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
+ updateConnectionState(R.string.connected);
+ invalidateOptionsMenu();
+ } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
+ mConnected = false;
+ updateConnectionState(R.string.disconnected);
+ invalidateOptionsMenu();
+ btDevice = null;
+ mDeviceAddress = null;
+ clearUI();
+ } else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
+ // Show all the supported services and characteristics on the user interface.
+ displayGattServices(mBluetoothLeService.getSupportedGattServices());
+ } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
+ displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
+ }
+ }
+ };
+
+ private static IntentFilter makeGattUpdateIntentFilter() {
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);
+ intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);
+ intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED);
+ intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE);
+ return intentFilter;
+ }
+
+ public void reload_config() {
+ String config_list_str = sharedPreferences.getString("config_list", "");
+ // if not config spi
+ if (config_list_str.isEmpty()) {
+ // set to default
+ mIndexSpiConfig.setText("首次使用,请先进菜单选择SPI管脚配置 ↗");
+ } else {
+ System.out.println(config_list_str);
+ try {
+ JSONArray jsonArray = new JSONArray(config_list_str);
+ JSONObject jsonObject;
+ for (int i = 0; i < jsonArray.length(); i++) {
+ jsonObject = jsonArray.getJSONObject(i);
+ boolean is_select = jsonObject.getBoolean("is_select");
+ // skip unselected
+ if (!is_select) {
+ continue;
+ }
+ String config_name = jsonObject.optString("config_name");
+ din_pin = jsonObject.getInt("din");
+ sck_pin = jsonObject.getInt("sck");
+ cs_pin = jsonObject.getInt("cs");
+ dc_pin = jsonObject.getInt("dc");
+ rst_pin = jsonObject.getInt("rst");
+ busy_pin = jsonObject.getInt("busy");
+ System.out.println(config_name + ":" + "din=" + din_pin + ", sck=" + sck_pin + ", cs=" + cs_pin
+ + ", dc=" + dc_pin + ", rst=" + rst_pin + ", busy=" + busy_pin + ", is_select=" + is_select);
+
+ SpiPinConfigInfo configInfo = new SpiPinConfigInfo(config_name, din_pin, sck_pin, cs_pin, dc_pin, rst_pin, busy_pin, is_select);
+ mIndexSpiConfig.setText(configInfo.getConfigVerbose());
+ return;
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void onScan(View view) {
+ // Open bluetooth devices scanning activity
+ //-----------------------------------------------------
+ startActivityForResult(
+ new Intent(this, DeviceScanActivity.class),
+ REQ_BLUETOOTH_CONNECTION);
+ }
+
+ public void onDisplay(View view)
+ {
+ // Open display selection activity
+ //-----------------------------------------------------
+ startActivityForResult(
+ new Intent(this, DisplaysActivity.class),
+ REQ_DISPLAY_SELECTION);
+ }
+
+ public void onLoad(View view)
+ {
+ startActivityForResult(
+ new Intent(this, CropImageActivity.class),
+ REQ_OPEN_FILE
+ );
+ }
+
+ // image process
+ public void onFilter(View view) {
+ // Check if any image is loaded
+ //-----------------------------------------------------
+ if (originalImage == null) {
+ PermissionHelper.note(this, R.string.no_pict);
+ }
+ // Check if any display is selected
+ //-----------------------------------------------------
+ else if (EPaperDisplay.epdInd == -1) {
+ PermissionHelper.note(this, R.string.no_disp);
+ }
+
+ // Open palette selection activity
+ //-----------------------------------------------------
+ else {
+ startActivityForResult(
+ new Intent(this, FilteringActivity.class),
+ REQ_PALETTE_SELECTION);
+ }
+ }
+
+ // Code to manage Service lifecycle.
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder service) {
+ mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService();
+ if (!mBluetoothLeService.initialize()) {
+ Log.e(TAG, "Unable to initialize Bluetooth");
+ finish();
+ }
+ Log.e(TAG, "initialize Bluetooth Le Service");
+ // Automatically connects to the device upon successful start-up initialization.
+ mBluetoothLeService.connect(mDeviceAddress);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName componentName) {
+ mBluetoothLeService = null;
+ }
+ };
+
+ private void displayData(String data) {
+ Log.e(TAG, "displayData:" + data);
+ if (data != null) {
+ try {
+ JSONObject jsonObject = new JSONObject(data);
+ mEsp32IpAddress = jsonObject.optString("ip_address");
+ mEsp32HostName = jsonObject.optString("host_name");
+ tvIpAddress.setText(mEsp32IpAddress);
+ tvHostName.setText(mEsp32HostName);
+ btnUploadWifi.setEnabled(true);
+ Log.e(TAG, "ip_address:" + mEsp32IpAddress + ", host_name:" + mEsp32HostName);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // Demonstrates how to iterate through the supported GATT Services/Characteristics.
+ // In this sample, we populate the data structure that is bound to the ExpandableListView
+ // on the UI.
+ private void displayGattServices(List gattServices) {
+ if (gattServices == null) return;
+ String uuid = null;
+ String unknownServiceString = getResources().getString(R.string.unknown_service);
+ String unknownCharaString = getResources().getString(R.string.unknown_characteristic);
+ String serviceName = "E-Paper";
+ String characterReadName = "Read WiFi IP";
+ String characterWriteMsgName = "Write msg";
+ String characterWriteDataName = "Write data";
+ String characterName = "";
+ ArrayList> gattServiceData = new ArrayList>();
+ ArrayList>> gattCharacteristicData
+ = new ArrayList>>();
+ mGattCharacteristics = new ArrayList>();
+
+ // Loops through available GATT Services.
+ // iter GATT service
+ for (BluetoothGattService gattService : gattServices) {
+ HashMap currentServiceData = new HashMap();
+ // get UUID
+ uuid = gattService.getUuid().toString();
+ // compare
+ if (!SampleGattAttributes.lookup(uuid, unknownServiceString).equals(serviceName)) {
+ continue;
+ }
+
+ ArrayList> gattCharacteristicGroupData =
+ new ArrayList>();
+ // get characteristics list
+ List gattCharacteristics =
+ gattService.getCharacteristics();
+ ArrayList charas =
+ new ArrayList();
+
+ // Loops through available Characteristics.
+ for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) {
+ charas.add(gattCharacteristic);
+ HashMap currentCharaData = new HashMap();
+ // get & compare UUID
+ uuid = gattCharacteristic.getUuid().toString();
+ characterName = SampleGattAttributes.lookup(uuid, unknownCharaString);
+ if (characterName.equals(characterReadName)) {
+ mCharacterRead = gattCharacteristic;
+ mBluetoothLeService.readCharacteristic(gattCharacteristic);
+ } else if (characterName.equals(characterWriteMsgName)) {
+ mCharacterWriteMsg = gattCharacteristic;
+ } else if (characterName.equals(characterWriteDataName)) {
+ mCharacterWriteData = gattCharacteristic;
+ } else {
+ continue;
+ }
+ }
+ mGattCharacteristics.add(charas);
+ gattCharacteristicData.add(gattCharacteristicGroupData);
+ }
+ }
+
+ public static void sendData(byte[] buffArr) {
+ mCharacterWriteData.setValue(buffArr);
+ mBluetoothLeService.writeCharacteristic(mCharacterWriteData);
+ }
+
+ private static void updateConnectionState(final int resourceId) {
+ if (resourceId == R.string.connected) {
+ tvConnectionState.setTextColor(Color.GREEN);
+ mConnected = true;
+ btnUploadBt.setEnabled(true);
+ btnLoadDefault.setEnabled(true);
+ } else {
+ tvConnectionState.setTextColor(Color.RED);
+ mConnected = false;
+ btnUploadBt.setEnabled(false);
+ btnLoadDefault.setEnabled(false);
+
+ // clear wifi params when disconnected
+ clearWifiParams();
+ }
+ tvConnectionState.setText(resourceId);
+ }
+
+ private static void clearWifiParams() {
+ tvIpAddress.setText("");
+ tvHostName.setText("");
+ }
+
+ private static void updateWifiParams(String rcvData) {
+ try {
+ JSONObject jsonObject = new JSONObject(rcvData);
+ tvIpAddress.setText(jsonObject.optString("IP_ADDRESS"));
+ tvHostName.setText(jsonObject.optString("HOST_NAME"));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ public void onUploadBt(View view) {
+ int epdInd = EPaperDisplay.epdInd;
+ boolean is_compress;
+ if (cbIsCompress.isChecked()) {
+ is_compress = true;
+ } else {
+ is_compress = false;
+ }
+ // keep screen on
+// view.setKeepScreenOn(true);
+ handlerBt = new SocketHandlerBt();
+ handlerBt.init_image(indTableImage, is_compress, null);
+ }
+
+ public void onUploadWifi(View view) {
+ int epdInd = EPaperDisplay.epdInd;
+ int width = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width;
+ int height = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height;
+
+ boolean is_compress;
+ if (cbIsCompress.isChecked()) {
+ is_compress = true;
+ } else {
+ is_compress = false;
+ }
+
+ handlerWifi = new SocketHandlerWifi(mEsp32IpAddress, mEsp32HostName);
+ handlerWifi.init_image(indTableImage, is_compress);
+
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Log.e(TAG, "http write start");
+ // EPD: initial
+ // LOADA: send phase 1 data
+ // LOADB: send phase 2 data
+ // SHOW: display image
+ String uri = "EPD";
+ try {
+ // init params include: epd index and spi pin configuration
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("epdInd", epdInd);
+ jsonObject.put("din", din_pin);
+ jsonObject.put("sck", sck_pin);
+ jsonObject.put("cs", cs_pin);
+ jsonObject.put("dc", dc_pin);
+ jsonObject.put("rst", rst_pin);
+ jsonObject.put("busy", busy_pin);
+ jsonObject.put("width", width);
+ jsonObject.put("height", height);
+
+ String post_data = jsonObject.toString();
+
+ handlerWifi.httpWrite(uri, post_data);
+
+ boolean is_continue = true;
+ while (is_continue) {
+ is_continue = handlerWifi.handleUploadingStage();
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }).start();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ Log.e(TAG, "requestCode:" + requestCode + ", resultCode:" + resultCode);
+ //-----------------------------------------------------
+ // Messages form ScanningActivity
+ //-----------------------------------------------------
+ if (requestCode == REQ_BLUETOOTH_CONNECTION) {
+ // Bluetooth device was found and selected
+ //-------------------------------------------------
+ if (resultCode == RESULT_OK) {
+ // Get selected bluetooth device
+ //---------------------------------------------
+ btDevice = data.getParcelableExtra("DEVICE");
+
+ // Show name and address of the device
+ //---------------------------------------------
+ textBlue.setText(btDevice.getName() + " (" + btDevice.getAddress() + ")");
+
+ // device address
+ mDeviceAddress = btDevice.getAddress();
+ tvDevAddress.setText(mDeviceAddress);
+
+ //--------------------------------------
+ tvUploadRatio.setText("0%");
+
+ Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
+ boolean bindResult = bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
+ Log.e(TAG, "mServiceConnection: " + mServiceConnection + " bindResult: " + bindResult);
+
+ if (mBluetoothLeService != null) {
+ final boolean result = mBluetoothLeService.connect(mDeviceAddress);
+ Log.d(TAG, "Connect request result=" + result);
+ }
+ }
+ }
+
+ //-----------------------------------------------------
+ // Message form open file activity
+ //-----------------------------------------------------
+ else if (requestCode == REQ_OPEN_FILE) {
+ if (resultCode == RESULT_OK) {
+ // Getting file name and file path
+ //---------------------------------------------
+ Uri image_uri = data.getParcelableExtra("FILE_NAME");
+ textLoad.setText(image_uri.getPath());
+
+ // Loading of the selected file
+ //---------------------------------------------
+ InputStream is = null;
+ try {
+ int width = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width;
+ int height = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height;
+
+ originalImage = MediaStore.Images.Media.getBitmap(this.getContentResolver(), image_uri);
+ originalImage = Bitmap.createScaledBitmap(originalImage, width, height, false);
+ int pictWidth = originalImage.getWidth();
+ int pictHeight = originalImage.getHeight();
+ pictFile.setMaxHeight(Math.min(300, pictWidth));
+ pictFile.setMinimumHeight(Math.min(200, pictWidth / 2));
+ pictFile.setImageBitmap(originalImage);
+ textLoad.setText("width:" + pictWidth + ", height:" + pictHeight);
+ } catch (FileNotFoundException e) {
+ textFilt.setText(R.string.failed_file);
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+ textFilt.setText(R.string.failed_file);
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ //-----------------------------------------------------
+ // Message form display selection activity
+ //-----------------------------------------------------
+ else if (requestCode == REQ_DISPLAY_SELECTION) {
+ if (resultCode == RESULT_OK) {
+ textDisp.setText(EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].title);
+ button_file.setEnabled(true);
+ }
+ }
+
+ //-----------------------------------------------------
+ // Message form palette selection activity
+ //-----------------------------------------------------
+ else if (requestCode == REQ_PALETTE_SELECTION) {
+ if (resultCode == RESULT_OK) {
+ textFilt.setText(data.getStringExtra("NAME"));
+
+ try {
+ int size = pictFile.getHeight();
+ pictFilt.setMaxHeight(Math.min(300, size));
+ pictFilt.setMinimumHeight(Math.min(200, size / 2));
+ pictFilt.setImageBitmap(indTableImage);
+ } catch (Exception e) {
+ textFilt.setText(R.string.failed_filt);
+ }
+ }
+ } else if (requestCode == SPI_PIN_CONFIG) {
+ if (resultCode == RESULT_OK) {
+ reload_config();
+ }
+ }
+ }
+
+ private void clearUI() {
+ tvDevAddress.setText("");
+ tvIpAddress.setText("");
+ tvHostName.setText("");
+ }
+
+ @SuppressLint("HandlerLeak")
+ public static Handler mUIHandler = new Handler(){
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ super.handleMessage(msg);
+ if (msg.what == UPDATE_PROGESS) {
+ tvUploadRatio.setText(msg.arg1 + "%");
+ pbUploadProgress.setProgress(msg.arg1);
+ } else if (msg.what == UPDATE_SEND_INFO) {
+ tvSendInfo.setText((String)(msg.obj));
+ } else if (msg.what == CONNECT_SUCCESS) {
+ updateConnectionState(R.string.connected);
+ } else if (msg.what == CONNECT_FAILURE) {
+ updateConnectionState(R.string.connect_fail);
+ } else if (msg.what == DISCONNECT_SUCCESS) {
+ updateConnectionState(R.string.disconnected);
+ } else if (msg.what == UPDATE_WIFI_PARAMS) {
+ updateWifiParams((String)msg.obj);
+ }
+ }
+ };
+
+ public static void updateProgress(int progress) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Message msg = new Message();
+ msg.what = UPDATE_PROGESS;
+ msg.arg1 = progress;
+ mUIHandler.sendMessage(msg);
+ }
+ }).start();
+ }
+
+
+ public static void updateSendInfo(String info) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Message msg = new Message();
+ msg.what = UPDATE_SEND_INFO;
+ msg.obj = info;
+ mUIHandler.sendMessage(msg);
+ }
+ }).start();
+ }
+
+
+ public void saveImage() {
+ // Check if any image is loaded
+ //-----------------------------------------------------
+ if (originalImage == null) {
+ PermissionHelper.note(this, R.string.no_pict);
+ }
+ // Check if any display is selected
+ //-----------------------------------------------------
+ else if (EPaperDisplay.epdInd == -1) {
+ PermissionHelper.note(this, R.string.no_disp);
+ }
+ else if (indTableImage == null) {
+ PermissionHelper.note(this, R.string.no_filt);
+ } else {
+ int h = indTableImage.getHeight();
+ int w = indTableImage.getWidth();
+ int[] saved_image_data = new int[w*h];
+ int i = 0;
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++, i++)
+ saved_image_data[i] = indTableImage.getPixel(x, y);
+ Gson gson = new Gson();
+ String saved_image_str = gson.toJson(saved_image_data);
+ config_editor.putString("saved_image_" + EPaperDisplay.epdInd, saved_image_str);
+ config_editor.commit();
+ Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ public void loadDefault(View view) {
+ if (EPaperDisplay.epdInd == -1) {
+ PermissionHelper.note(this, R.string.no_disp);
+ return;
+ }
+
+ String saved_image_str = sharedPreferences.getString("saved_image_" + EPaperDisplay.epdInd, "");
+ if (saved_image_str.isEmpty()) {
+ Toast.makeText(this, "没有找到图像,请先保存", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Gson gson = new Gson();
+ int[] saved_image_data = gson.fromJson(saved_image_str, int [].class);
+ Toast.makeText(this, "像素数:" + saved_image_data.length, Toast.LENGTH_SHORT).show();
+ System.out.println(saved_image_data.length);
+
+ boolean is_compress = cbIsCompress.isChecked();
+ // keep screen on
+// view.setKeepScreenOn(true);
+ handlerBt = new SocketHandlerBt();
+ handlerBt.init_image(MainActivity.indTableImage, is_compress, saved_image_data);
+ }
+
+ public void rebootDevice() {
+ if (btDevice == null) {
+ Toast.makeText(this, "蓝牙设备未连接", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Toast.makeText(this, "设备重启中...", Toast.LENGTH_SHORT).show();
+
+ JSONObject jsonObject = new JSONObject();
+ try {
+ jsonObject.put("COMMAND", "reboot_device");
+ byte[] send_bytes = jsonObject.toString().getBytes(StandardCharsets.UTF_8);
+ sendData(send_bytes);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/epaperuploader/SampleGattAttributes.java b/app/src/main/java/com/example/epaperuploader/SampleGattAttributes.java
new file mode 100644
index 0000000..386d9d2
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/SampleGattAttributes.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.epaperuploader;
+
+import java.util.HashMap;
+
+/**
+ * This class includes a small subset of standard GATT attributes for demonstration purposes.
+ */
+public class SampleGattAttributes {
+ private static HashMap attributes = new HashMap();
+ public static String HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb";
+ public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";
+
+ static {
+ // Sample Services.
+ attributes.put("0000180d-0000-1000-8000-00805f9b34fb", "Heart Rate Service");
+ attributes.put("0000180a-0000-1000-8000-00805f9b34fb", "Device Information Service");
+ // Sample Characteristics.
+ attributes.put(HEART_RATE_MEASUREMENT, "Heart Rate Measurement");
+ attributes.put("00002a29-0000-1000-8000-00805f9b34fb", "Manufacturer Name String");
+ // 增加墨水屏的UUID定制
+ attributes.put("6e400001-b5a3-f393-e0a9-e50e24dcca9e", "E-Paper");
+ attributes.put("6e400003-b5a3-f393-e0a9-e50e24dcca9e", "Read WiFi IP");
+ attributes.put("6e400004-b5a3-f393-e0a9-e50e24dcca9e", "Write msg");
+ attributes.put("6e400005-b5a3-f393-e0a9-e50e24dcca9e", "Write data");
+
+ }
+
+ public static String lookup(String uuid, String defaultName) {
+ String name = attributes.get(uuid);
+ return name == null ? defaultName : name;
+ }
+}
diff --git a/app/src/main/java/com/example/epaperuploader/SocketHandlerBt.java b/app/src/main/java/com/example/epaperuploader/SocketHandlerBt.java
new file mode 100644
index 0000000..3c746d5
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/SocketHandlerBt.java
@@ -0,0 +1,622 @@
+package com.example.epaperuploader;
+
+import static com.example.epaperuploader.MainActivity.busy_pin;
+import static com.example.epaperuploader.MainActivity.cs_pin;
+import static com.example.epaperuploader.MainActivity.dc_pin;
+import static com.example.epaperuploader.MainActivity.din_pin;
+import static com.example.epaperuploader.MainActivity.rst_pin;
+import static com.example.epaperuploader.MainActivity.sck_pin;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Handler;
+import android.util.Log;
+
+import com.example.epaperuploader.image_processing.EPaperDisplay;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+//---------------------------------------------------------
+// Socket Handler
+//---------------------------------------------------------
+class SocketHandlerBt extends Handler
+{
+
+ // Uploaded data buffer
+ //---------------------------------------------------------
+ private static final int BUFF_SIZE = 512;
+ private static final int BUFF_SIZE_WIFI = 496;
+ private static byte[] buffArr = new byte[BUFF_SIZE];
+ private static int buffInd;
+ private static int xLine;
+ private static String rqMsg;
+
+ private int pxInd; // Pixel index in picture
+ private int stInd; // Stage index of uploading
+ private int dSize; // Size of uploaded data by LOAD command
+ private int[] array; // Values of picture pixels
+
+ private int sendSize;
+ private boolean next_stage; // 是否分两批发送: 双色或七色fasle,三色true
+ private boolean load_stage = false;
+ private boolean mIsCompress = false;
+ private byte[] compress_data;
+ private byte[] compress_data2;
+
+ private final static String TAG = SocketHandlerBt.class.getSimpleName();
+
+ public SocketHandlerBt()
+ {
+ super();
+ }
+
+ // 图像数据压缩
+ byte[] compress_image_data(byte[] image_array) {
+ // 输出字节流
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ // GZip输出流
+ GZIPOutputStream gzipOutputStream;
+ try {
+ // 将图像数据导入GZip
+ gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
+ gzipOutputStream.write(image_array);
+ // 关闭GZip输出流
+ gzipOutputStream.close();
+ // 记录压缩后的字节流
+ byte[] encode_array = byteArrayOutputStream.toByteArray();
+ // 释放输出字节流
+ byteArrayOutputStream.flush();
+ byteArrayOutputStream.close();
+ System.out.println("gzip compress len=" + encode_array.length);
+ // 打印压缩后的数据
+ return encode_array;
+ } catch (IOException e) {
+ System.out.println("gzip compress error."+e);
+ return null;
+ }
+ }
+
+ // 解压缩图像数据
+ byte[] decompress_image_data(byte[] encode_array) {
+ try {
+ // 定义临时缓冲区
+ byte[] buffer = new byte[1024];
+ byte[] image_data;
+
+ // 输出字节流
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 输入字节流
+ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encode_array);
+ // GZip输入流
+ GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream);
+ // 批量写入输出字节流
+ int n = 0;
+ while ((n = gzipInputStream.read(buffer, 0, buffer.length)) > 0) {
+ outputStream.write(buffer, 0, n);
+ }
+
+ // 打印解压后的数据
+ System.out.println("gzip decompress len=" + outputStream.toByteArray().length);
+
+ image_data = outputStream.toByteArray();
+ // 释放流资源
+ outputStream.close();
+ byteArrayInputStream.close();
+ gzipInputStream.close();
+ return image_data;
+ } catch (IOException e) {
+ System.out.println("gzip decompress error."+e);
+ return null;
+ }
+ }
+
+ private byte[] get_compress_data(int epdInd, int c) {
+ byte[] byteArr;
+ int i;
+ int index = 0;
+ int pixelId = 0;
+
+ if(epdInd == 25 || epdInd ==37 || epdInd == 43 || epdInd == 47) {
+ byteArr = new byte[array.length / 2];
+ while (pixelId < array.length) {
+
+ int v = 0;
+ // 从发送数据的角度看,每个字节高4bit和低4bit可以分别存储一个像素颜色值(0~7)
+ // 所以4个pixel占用2个存储单元
+ for (i = 0; i < 16; i += 4) {
+ if (pixelId < array.length) v |= (array[pixelId] << i);
+ pixelId++;
+ }
+
+ byteArr[index++] = (byte) (v);
+ byteArr[index++] = (byte) (v >> 8);
+ }
+ } else if(epdInd == 45) {
+ byteArr = new byte[array.length/4];
+ while (pixelId < array.length)
+ {
+ int v = 0;
+
+ for (i = 0; i < 8; i += 2)
+ {
+ if (pixelId < array.length) v |= (array[pixelId] << (6-i));
+ pixelId++;
+ }
+
+ byteArr[index++] = (byte)v;
+ }
+ } else {
+ byteArr = new byte[array.length/8];
+ while (pixelId < array.length)
+ {
+ int v = 0;
+
+ for (i = 0; i < 8; i++)
+ {
+ if ((pixelId < array.length) && (array[pixelId] != c)) v |= (128 >> i);
+ pixelId++;
+ }
+
+ byteArr[index++] = (byte)v;
+ }
+ }
+
+ return byteArr;
+ }
+
+ // Converts picture pixels into selected pixel format
+ // and sends EPDx command
+ //-----------------------------------------------------
+ public boolean init_image(Bitmap bmp, boolean is_compress, int[] saved_image_data)
+ {
+ int w, h;
+ int epdInd = EPaperDisplay.epdInd;
+
+ // Loading pixels into array
+ //-------------------------------------------------
+ System.out.println("=========init_image is calling ==========");
+
+ if (saved_image_data == null) {
+ w = bmp.getWidth();
+ h = bmp.getHeight();
+ } else {
+ w = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width;
+ h = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height;
+ }
+
+ array = new int[w*h]; // Array of pixels
+ int i = 0; // Index of pixel in the array of pixels
+
+ if (saved_image_data == null) {
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++, i++)
+ if(epdInd == 25 || epdInd ==37 || epdInd == 43 || epdInd == 47)
+ array[i] = getVal_7color(bmp.getPixel(x, y));
+ else
+ array[i] = getVal(bmp.getPixel(x, y));
+ } else {
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++, i++)
+ if(epdInd == 25 || epdInd ==37 || epdInd == 43 || epdInd == 47)
+ array[i] = getVal_7color(saved_image_data[i]);
+ else
+ array[i] = getVal(saved_image_data[i]);
+ }
+
+ byte[] byteArr;
+// byte[] decompressed_data;
+
+ System.out.println("image array size: " + array.length);
+
+ mIsCompress = is_compress;
+
+ if (mIsCompress)
+ {
+ // re-init
+ compress_data = null;
+ compress_data2 = null;
+
+ // 数据压缩
+ int c = 0;
+ // 先根据不同屏幕,获取编码数据
+ byteArr = get_compress_data(epdInd, c);
+ // 再对编码结果进行压缩
+ compress_data = compress_image_data(byteArr);
+// decompressed_data = decompress_image_data(compress_data);
+
+ // 如果是三色屏的话,还需要再准备第二批代表红色的数据
+ if ((epdInd == 3) || (epdInd == 39) || (epdInd==0)||(epdInd==6)||(epdInd==7)||(epdInd==9)||(epdInd==12)||
+ (epdInd==16)||(epdInd==19)||(epdInd==22)||(epdInd==26)||(epdInd==27)||(epdInd==28)){
+ // do nothing
+ } else if (epdInd>15 && epdInd < 22) {
+ // do nothing
+ } else if (epdInd == 25 || epdInd == 37 || epdInd == 43 || epdInd == 47) {
+ // do nothing
+ } else {
+ c = 3;
+ byteArr = get_compress_data(epdInd, c);
+ compress_data2 = compress_image_data(byteArr);
+// decompressed_data = decompress_image_data(compress_data2);
+ }
+ }
+
+ pxInd = 0; // pixel id
+ xLine = 0; //2.13inch
+ stInd = 0; // step id
+ dSize = 0; // nof data size
+
+ sendSize = 0; // nof send size
+ load_stage = false;
+ next_stage = false; // if has next stage
+
+// buffInd = 9; // Size of command in bytes
+ buffInd = 13; // Size of command in bytes
+ buffArr[0] = (byte)'I'; // Name of command (Initialize)
+ buffArr[1] = (byte)EPaperDisplay.epdInd; // Index of display
+
+ // 数据是否压缩
+ if (mIsCompress) {
+ buffArr[2] = (byte)1;
+ } else {
+ buffArr[2] = (byte)0;
+ }
+ // SPI管脚信息
+ buffArr[3] = (byte)din_pin;
+ buffArr[4] = (byte)sck_pin;
+ buffArr[5] = (byte)cs_pin;
+ buffArr[6] = (byte)dc_pin;
+ buffArr[7] = (byte)rst_pin;
+ buffArr[8] = (byte)busy_pin;
+
+ //图像尺寸
+ buffArr[9] = (byte)(EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width >> 8);
+ buffArr[10] = (byte)EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width;
+ buffArr[11] = (byte)(EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height >> 8);
+ buffArr[12] = (byte)EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height;
+
+ return u_send(false);
+// return true;
+ }
+
+ // The function is executed after every "Ok!" response
+ // obtained from esp32, which means a previous command
+ // is complete and esp32 is ready to get the new one.
+ //-----------------------------------------------------
+ // 这个接口每调用一次,就批量发送一批数据,循环往复直到发送完毕
+ public boolean handleUploadingStage()
+ {
+ int epdInd = EPaperDisplay.epdInd;
+
+ // 2.13 e-Paper display
+ if ((epdInd == 3) || (epdInd == 39))
+ {
+ if(stInd == 0) return u_line(0, 0, 100);
+ //-------------------------------------------------
+ if(stInd == 1) return u_show();
+ }
+
+ // 2.13 b V4
+ else if ((epdInd == 40))
+ {
+ if(stInd == 0) return u_line(0, 0, 50);
+ if(stInd == 1) return u_next();
+ if(stInd == 2) return u_line(3, 50, 50);
+ if(stInd == 3) return u_show();
+ }
+
+ // White-black e-Paper displays
+ //-------------------------------------------------
+ else if ((epdInd==0)||(epdInd==6)||(epdInd==7)||(epdInd==9)||(epdInd==12)||
+ (epdInd==16)||(epdInd==19)||(epdInd==22)||(epdInd==26)||(epdInd==27)||(epdInd==28))
+ {
+ if(stInd == 0) return u_data(0,0,100);
+ if(stInd == 1) return u_show();
+ }
+
+ // 7.5 colored e-Paper displays
+ //-------------------------------------------------
+ else if ((epdInd>15 && epdInd < 22) || (epdInd == 45))
+ {
+ if(stInd == 0) return u_data(-1,0,100);
+ if(stInd == 1) return u_show();
+ }
+
+ // 5.65f colored e-Paper displays
+ //-------------------------------------------------
+ else if (epdInd == 25 || epdInd == 37 || epdInd == 43 || epdInd == 47)
+ {
+ if(stInd == 0) return u_data(-2,0,100);
+ if(stInd == 1) return u_show();
+ }
+
+ // Other colored e-Paper displays
+ //-------------------------------------------------
+ else
+ {
+ if(stInd == 0)return u_data((epdInd == 1)? -1 : 0,0,50);
+ if(stInd == 1)return u_next();
+ if(stInd == 2)return u_data(3,50,50);
+ if(stInd == 3)return u_show();
+ }
+
+ return true;
+ }
+
+ // Returns the index of color in palette
+ //-----------------------------------------------------
+ public int getVal(int color)
+ {
+ int r = Color.red(color);
+ int g = Color.green(color);
+ int b = Color.blue(color);
+
+// 黑色:对应00b
+// 白色:对应01b
+// 黄色:对应10b
+// 红色:对应11b
+// @ColorInt public static final int BLACK = 0xFF000000;
+// @ColorInt public static final int WHITE = 0xFFFFFFFF;
+// @ColorInt public static final int RED = 0xFFFF0000;
+// @ColorInt public static final int YELLOW = 0xFFFFFF00;
+ if((r == 0xFF) && (b == 0xFF)) return 1;
+ if((r == 0x7F) && (b == 0x7F)) return 2;
+ if((r == 0xFF) && (g == 0xFF)) return 2;
+ if((r == 0xFF) && (b == 0x00)) return 3;
+
+ return 0;
+ }
+
+ // Returns the index of color in palette just for 5.65f e-Paper
+ //-----------------------------------------------------
+ public int getVal_7color(int color)
+ {
+ int r = Color.red(color);
+ int g = Color.green(color);
+ int b = Color.blue(color);
+
+ if((r == 0x00) && (g == 0x00) && (b == 0x00)) return 0;
+ if((r == 0xFF) && (g == 0xFF) && (b == 0xFF)) return 1;
+ if((r == 0x00) && (g == 0xFF) && (b == 0x00)) return 2;
+ if((r == 0x00) && (g == 0x00) && (b == 0xFF)) return 3;
+ if((r == 0xFF) && (g == 0x00) && (b == 0x00)) return 4;
+ if((r == 0xFF) && (g == 0xFF) && (b == 0x00)) return 5;
+ if((r == 0xFF) && (g == 0x80) && (b == 0x00)) return 6;
+
+ return 7;
+ }
+
+ // Sends command cmd
+ //-----------------------------------------------------
+ private boolean u_send(boolean next)
+ {
+ byte[] sendArr = new byte[buffInd];
+ System.arraycopy(buffArr, 0, sendArr, 0, buffInd);
+ MainActivity.sendData(sendArr);
+
+ if(next) stInd++; // Go to next stage if it is needed
+ return true; // Command is sent successful
+ }
+
+ // Next stage command
+ //-----------------------------------------------------
+ private boolean u_next()
+ {
+ Log.e(TAG, "u_next: " + stInd);
+ buffInd = 1; // Size of command in bytes
+ buffArr[0] = (byte)'N';// Name of command (Next)
+
+ pxInd = 0;
+ load_stage = false;
+ next_stage = true;
+ return u_send(true);
+ }
+
+ // The finishing command
+ //-----------------------------------------------------
+ private boolean u_show()
+ {
+ buffInd = 1; // Size of command in bytes
+ buffArr[0] = (byte)'S';// Name of command (Show picture)
+
+ // Return false if the SHOW command is not sent
+ //-------------------------------------------------
+ if (!u_send(true)) return false;
+
+ // Otherwise exit the uploading activity.
+ //-------------------------------------------------
+ return true;
+ }
+
+ // Sends pixels of picture and shows uploading progress
+ //-----------------------------------------------------
+ private boolean u_load(int k1, int k2)
+ {
+ // Uploading progress message
+ //-------------------------------------------------
+ // 计算出已发送的数据占全部数据的百分比
+ // pxInd=1830, x=0+100*1830/122*250=6%
+ int progress = 0;
+ if (mIsCompress) {
+ // 两批压缩的数据长度可能会不一样,但是进度暂时按各50%统计
+ if (!next_stage) {
+ progress = (k1 + k2*pxInd/compress_data.length);
+ } else {
+ progress = (k1 + k2*pxInd/compress_data2.length);
+ }
+ } else {
+ progress = (k1 + k2*pxInd/array.length);
+ }
+ String x = "" + progress;
+
+ if (x.length() > 5) x = x.substring(0, 5);
+ MainActivity.updateProgress(progress);
+
+ // Size of uploaded data
+ //-------------------------------------------------
+ dSize += buffInd;
+ sendSize += buffInd;
+
+ String send_info;
+ if (mIsCompress) {
+ if (next_stage) {
+ send_info = "像素:" + array.length + ",发送字节:" + sendSize + "/" + (compress_data.length + compress_data2.length);
+ } else {
+ send_info = "像素:" + array.length + ",发送字节:" + sendSize + "/" + compress_data.length;
+ }
+ } else {
+ send_info = "像素:" + array.length + ",发送字节:" + sendSize;
+ }
+ MainActivity.updateSendInfo(send_info);
+
+ if (mIsCompress) {
+ if (!next_stage) {
+ return u_send(pxInd >= compress_data.length);
+ } else {
+ return u_send(pxInd >= compress_data2.length);
+ }
+ } else {
+ return u_send(pxInd >= array.length);
+ }
+ }
+
+ // Pixel format converting
+ //-----------------------------------------------------
+ private boolean u_data(int c, int k1, int k2)
+ {
+ if (!load_stage) {
+ buffArr[0] = (byte)'L';
+ buffInd = 1;
+ load_stage = true;
+// System.out.println("u_data: LOAD");
+ return u_send(false);
+ }
+
+ buffInd = 0;
+
+ if(c == -1)
+ {
+ if (mIsCompress) {
+ for (; (pxInd < compress_data.length) && (buffInd < BUFF_SIZE); pxInd++) {
+// buffArr[buffInd++] = compress_data[pxInd++];
+ buffArr[buffInd++] = compress_data[pxInd];
+ }
+ } else {
+ while ((pxInd < array.length) && (buffInd + 1 < BUFF_SIZE)) {
+ int v = 0;
+
+ for (int i = 0; i < 16; i += 2) {
+ if (pxInd < array.length) v |= (array[pxInd] << i);
+ pxInd++;
+ }
+
+ buffArr[buffInd++] = (byte) (v);
+ buffArr[buffInd++] = (byte) (v >> 8);
+ }
+ }
+ }
+ else if(c == -2)
+ {
+ if (mIsCompress) {
+ for (; (pxInd < compress_data.length) && (buffInd < BUFF_SIZE); pxInd++) {
+// buffArr[buffInd++] = compress_data[pxInd++];
+ buffArr[buffInd++] = compress_data[pxInd];
+ }
+ } else {
+ // 遍历pixel数据,按照缓冲区大小来进行批量填充
+ while ((pxInd < array.length) && (buffInd + 1 < BUFF_SIZE))
+ {
+ int v = 0;
+
+ // 从发送数据的角度看,每个字节高4bit和低4bit可以分别存储一个像素颜色值(0~7)
+ // 所以4个pixel占用2个存储单元
+ for(int i = 0; i < 16; i += 4)
+ {
+ if (pxInd < array.length) v |= (array[pxInd] << i);
+ pxInd++;
+ }
+
+ buffArr[buffInd++] = (byte)(v );
+ buffArr[buffInd++] = (byte)(v >> 8);
+ }
+ }
+
+ }
+ else
+ {
+ if (mIsCompress) {
+ if (!next_stage) {
+ for (; (pxInd < compress_data.length) && (buffInd < BUFF_SIZE); pxInd++) {
+ buffArr[buffInd++] = compress_data[pxInd];
+ }
+ } else {
+ // c值为3,代表读取红色像素数据
+ for (; (pxInd < compress_data2.length) && (buffInd < BUFF_SIZE); pxInd++)
+ {
+ buffArr[buffInd++] = compress_data2[pxInd];
+ }
+ }
+ } else {
+ while ((pxInd < array.length) && (buffInd < BUFF_SIZE))
+ {
+ int v = 0;
+
+ for (int i = 0; i < 8; i++)
+ {
+ if ((pxInd < array.length) && (array[pxInd] != c)) v |= (128 >> i);
+ pxInd++;
+ }
+
+ buffArr[buffInd++] = (byte)v;
+ }
+ }
+ }
+
+ return u_load(k1, k2);
+ }
+
+ // Pixel format converting (2.13 e-Paper display)
+ //-----------------------------------------------------
+ private boolean u_line(int c, int k1, int k2)
+ {
+ if (!load_stage) {
+ buffArr[0] = (byte)'L';
+ buffInd = 1;
+ load_stage = true;
+ System.out.println("u_line: LOAD");
+ return u_send(false);
+ }
+
+ buffInd = 0;
+ // 分辨率 122 * 250
+ // 遍历像素数组
+ // 每行122个像素,需要用122/8=15.25≈16个字节; 246-6=240个字节; 可以存储240/16=15行数据
+ // 换句话讲,老程序默认缓冲区大小为256个字节,抛去头部的6个字节,就剩下250个字节,也只够存储15行数据
+ // 当把缓冲区扩大为512字节之后,最多可以存储31行,即有效数据字节 6 + 16 * 31 = 502
+ while ((pxInd < array.length) && (buffInd < BUFF_SIZE)) // 15*16+6 ,16*8 = 128
+ {
+ int v = 0;
+
+ // 逐行扫描,将每8个像素的0或1,压缩成一个0~255的字节,存储在bufferArr[]里
+ for (int i = 0; (i < 8) && (xLine < 122); i++, xLine++){
+ // [0,1,0,1,0,1...]
+ // i=0; array[0]=0; v=0
+ // i=1; array[1]=1; v=0 | 0x80>>1 = 0x40
+ // i=2; array[2]=0; v=0x40
+ // i=3; array[3]=1; v=0x40 | 0x80>>3 = 0x40 | 0x10 = 0x50
+ // i=4; array[4]=0; v=0x50
+ // i=5; array[5]=1; v=0x50 | 0x80>>5 = 0x50 | 0x4 = 0x54
+ // i=6; array[6]=0; v=0x54
+ // i=7; array[7]=1; v=0x54 | 0x80>>7 = 0x54 | 0x1 = 0x55
+ if (array[pxInd++] != c) v |= (128 >> i);
+ }
+ if(xLine >= 122 )xLine = 0;
+ buffArr[buffInd++] = (byte)v;
+ }
+
+ return u_load(k1, k2);
+ }
+}
diff --git a/app/src/main/java/com/example/epaperuploader/SocketHandlerWifi.java b/app/src/main/java/com/example/epaperuploader/SocketHandlerWifi.java
new file mode 100644
index 0000000..a60ccbe
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/SocketHandlerWifi.java
@@ -0,0 +1,653 @@
+package com.example.epaperuploader;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Handler;
+import android.util.Log;
+
+import com.example.epaperuploader.image_processing.EPaperDisplay;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Objects;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+class SocketHandlerWifi extends Handler
+{
+ private static final int BUFF_SIZE_WIFI = 10000;
+ private static int buffInd;
+ private static int xLine;
+ private static String rqMsg;
+
+ private int pxInd; // Pixel index in picture
+ private int stInd; // Stage index of uploading
+ private int dSize; // Size of uploaded data by LOAD command
+ private int[] array; // Values of picture pixels
+
+ private int sendSize;
+ private boolean next_stage; // 是否分两批发送: 双色或七色fasle,三色true
+ private boolean load_stage = false;
+ private boolean mIsCompress = false;
+ private byte[] compress_data;
+ private byte[] compress_data2;
+
+ private String mEsp32IpAddress = null;
+ private String mEsp32HostName = null;
+
+ private final static String TAG = SocketHandlerWifi.class.getSimpleName();
+
+ public SocketHandlerWifi(String ip_address, String host_hame)
+ {
+ super();
+ mEsp32IpAddress = ip_address;
+ mEsp32HostName = host_hame;
+ }
+
+ // 图像数据压缩
+ byte[] compress_image_data(byte[] image_array) {
+ // 输出字节流
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ // GZip输出流
+ GZIPOutputStream gzipOutputStream;
+ try {
+ // 将图像数据导入GZip
+ gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
+ gzipOutputStream.write(image_array);
+ // 关闭GZip输出流
+ gzipOutputStream.close();
+ // 记录压缩后的字节流
+ byte[] encode_array = byteArrayOutputStream.toByteArray();
+ // 释放输出字节流
+ byteArrayOutputStream.flush();
+ byteArrayOutputStream.close();
+ System.out.println("gzip compress len=" + encode_array.length);
+ return encode_array;
+ } catch (IOException e) {
+ System.out.println("gzip compress error."+e);
+ return null;
+ }
+ }
+
+ // 解压缩图像数据
+ byte[] decompress_image_data(byte[] encode_array) {
+ try {
+ // 定义临时缓冲区
+ byte[] buffer = new byte[1024];
+ byte[] image_data;
+
+ // 输出字节流
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // 输入字节流
+ ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encode_array);
+ // GZip输入流
+ GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream);
+ // 批量写入输出字节流
+ int n = 0;
+ while ((n = gzipInputStream.read(buffer, 0, buffer.length)) > 0) {
+ outputStream.write(buffer, 0, n);
+ }
+
+ // 打印解压后的数据
+ System.out.println("gzip decompress len=" + outputStream.toByteArray().length);
+
+ image_data = outputStream.toByteArray();
+ // 释放流资源
+ outputStream.close();
+ byteArrayInputStream.close();
+ gzipInputStream.close();
+ return image_data;
+ } catch (IOException e) {
+ System.out.println("gzip decompress error."+e);
+ return null;
+ }
+ }
+
+ private byte[] get_compress_data(int epdInd, int c) {
+ byte[] byteArr;
+ int i;
+ int index = 0;
+ int pixelId = 0;
+
+ if(epdInd == 25 || epdInd ==37 || epdInd == 43 || epdInd == 47) {
+ byteArr = new byte[array.length/2];
+ while (pixelId < array.length) {
+
+ int v = 0;
+ // 从发送数据的角度看,每个字节高4bit和低4bit可以分别存储一个像素颜色值(0~7)
+ // 所以4个pixel占用2个存储单元
+ for(i = 0; i < 16; i += 4)
+ {
+ if (pixelId < array.length) v |= (array[pixelId] << i);
+ pixelId++;
+ }
+
+ byteArr[index++] = (byte)(v );
+ byteArr[index++] = (byte)(v >> 8);
+ }
+
+ } else {
+ byteArr = new byte[array.length/8];
+ while (pixelId < array.length)
+ {
+ int v = 0;
+
+ for (i = 0; i < 8; i++)
+ {
+ if ((pixelId < array.length) && (array[pixelId] != c)) v |= (128 >> i);
+ pixelId++;
+ }
+
+ byteArr[index++] = (byte)v;
+ }
+ }
+
+ return byteArr;
+ }
+
+ // Converts picture pixels into selected pixel format
+ // and sends EPDx command
+ //-----------------------------------------------------
+ public boolean init_image(Bitmap bmp, boolean is_compress)
+ {
+ int w = bmp.getWidth(); // Picture with
+ int h = bmp.getHeight();// Picture height
+ int epdInd = EPaperDisplay.epdInd;
+ array = new int[w*h]; // Array of pixels
+ int i = 0; // Index of pixel in the array of pixels
+
+ Log.e(TAG, "init_image start");
+
+ // Loading pixels into array
+ //-------------------------------------------------
+ // 从处理过的图像里提取像素信息
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++, i++)
+ if(epdInd == 25 || epdInd ==37 || epdInd == 43) {
+ // 7-color: value 0~7
+ array[i] = getVal_7color(bmp.getPixel(x, y));
+ }
+ else if (epdInd == 47) {
+ // New 7-color: value 0~7
+ array[i] = getVal_new7color(bmp.getPixel(x, y));
+ } else {
+ // B/W/R: value 0~3
+ array[i] = getVal(bmp.getPixel(x, y));
+ }
+
+ byte[] byteArr;
+// byte[] decompressed_data;
+
+ System.out.println("image array size: " + array.length);
+
+ mIsCompress = is_compress;
+ // wifi模式不压缩
+ mIsCompress = false;
+ if (mIsCompress)
+ {
+ // re-init
+ compress_data = null;
+ compress_data2 = null;
+
+ // 数据压缩
+ int c = 0;
+ // 先根据不同屏幕,获取编码数据
+ byteArr = get_compress_data(epdInd, c);
+ // 再对编码结果进行压缩
+ compress_data = compress_image_data(byteArr);
+// decompressed_data = decompress_image_data(compress_data);
+
+ // 如果是三色屏的话,还需要再准备第二批代表红色的数据
+ if ((epdInd == 3) || (epdInd == 39) || (epdInd==0)||(epdInd==6)||(epdInd==7)||(epdInd==9)||(epdInd==12)||
+ (epdInd==16)||(epdInd==19)||(epdInd==22)||(epdInd==26)||(epdInd==27)||(epdInd==28)){
+ // do nothing
+ } else if ((epdInd>15 && epdInd < 22) || (epdInd == 45)) {
+ // do nothing
+ } else if (epdInd == 25 || epdInd == 37 || epdInd == 43 || epdInd == 47) {
+ // do nothing
+ } else {
+ c = 3;
+ byteArr = get_compress_data(epdInd, c);
+ compress_data2 = compress_image_data(byteArr);
+// decompressed_data = decompress_image_data(compress_data2);
+ }
+ }
+
+ String send_info = "像素:" + array.length + ",发送字节:0";
+ MainActivity.updateSendInfo(send_info);
+ MainActivity.updateProgress(0);
+
+ sendSize = 0; // nof send size
+ next_stage = false; // if has next stage
+
+ pxInd = 0;
+ xLine = 0; //2.13inch
+ stInd = 0;
+ dSize = 0;
+
+ Log.e(TAG, "init_image end");
+
+ return true;
+ }
+
+ void send_data(int k1, int k2, int total_data_size, int loaded_size, String uri, String post_data) throws IOException {
+ int progress = (k1 + k2*loaded_size/total_data_size);
+ String x = "" + progress;
+
+ if (x.length() > 5) x = x.substring(0, 5);
+
+ MainActivity.updateProgress(progress);
+
+ // Size of uploaded data
+ //-------------------------------------------------
+ String send_info = "像素:" + array.length + ",发送字节:" + loaded_size/2;
+
+ MainActivity.updateSendInfo(send_info);
+
+ httpWrite(uri, post_data);
+ }
+
+ // The function is executed after every "Ok!" response
+ // obtained from esp32, which means a previous command
+ // is complete and esp32 is ready to get the new one.
+ //-----------------------------------------------------
+ public boolean handle_uploading_12in48b(String imgBWStr, String imgRStr) throws IOException {
+ StringBuilder imgBWStr_m1s1m2s2 = new StringBuilder();
+ StringBuilder imgRStr_m1s1m2s2 = new StringBuilder();
+
+ int i = 0;
+ int start_index = 0;
+ int end_index = 0;
+ // 打包第一个区域
+ for (i = 0; i < 492; i++) {
+ start_index = i * 326;
+ end_index = start_index + 162;
+ imgBWStr_m1s1m2s2.append(imgBWStr.substring(start_index, end_index));
+ imgRStr_m1s1m2s2.append(imgRStr.substring(start_index, end_index));
+ }
+ // 打包第二个区域
+ for (i = 0; i < 492; i++) {
+ start_index = i * 326 + 162;
+ end_index = start_index + 164;
+ imgBWStr_m1s1m2s2.append(imgBWStr.substring(start_index, end_index));
+ imgRStr_m1s1m2s2.append(imgRStr.substring(start_index, end_index));
+ }
+ // 打包第三个区域
+ for (i = 492; i < 984; i++) {
+ start_index = i * 326;
+ end_index = start_index + 162;
+ imgBWStr_m1s1m2s2.append(imgBWStr.substring(start_index, end_index));
+ imgRStr_m1s1m2s2.append(imgRStr.substring(start_index, end_index));
+ }
+ // 打包第四个区域
+ for (i = 492; i < 984; i++) {
+ start_index = i * 326 + 162;
+ end_index = start_index + 164;
+ imgBWStr_m1s1m2s2.append(imgBWStr.substring(start_index, end_index));
+ imgRStr_m1s1m2s2.append(imgRStr.substring(start_index, end_index));
+ }
+
+ Log.e(TAG, "package for 12in48b");
+
+ int bw_data_size = imgBWStr_m1s1m2s2.length();
+ int r_data_size = imgRStr_m1s1m2s2.length();
+
+ Log.e(TAG, "bw_data_size=" + bw_data_size + ", r_data_size=" + r_data_size);
+
+ int batch_size = 30000;
+ String uri = "";
+ String post_data = "";
+ int k1 = 0;
+ int k2 = 50;
+ start_index = 0;
+ // 将第一阶段数据进行分批发送
+ while (start_index < bw_data_size) {
+ uri = "LOADA";
+ end_index = Math.min((start_index + batch_size), bw_data_size);
+ post_data = imgBWStr_m1s1m2s2.substring(start_index, end_index);
+ Log.e(TAG, "LOADA: " + start_index + "->" + end_index);
+ send_data(k1, k2, bw_data_size, end_index,uri, post_data);
+
+ start_index += batch_size;
+ }
+
+ Log.e(TAG, "LoadA finish");
+
+ k1 = 50;
+ k2 = 50;
+ start_index = 0;
+ // 再将第二阶段数据也分批发送
+ while (start_index < r_data_size) {
+ uri = "LOADB";
+ end_index = Math.min((start_index + batch_size), r_data_size);
+ post_data = imgRStr_m1s1m2s2.substring(start_index, end_index);
+ Log.e(TAG, "LOADB: " + start_index + "->" + end_index);
+ send_data(k1, k2, r_data_size, end_index,uri, post_data);
+
+ start_index += batch_size;
+ }
+ Log.e(TAG, "LoadB finish");
+
+ // 最后刷新显示
+ uri = "SHOW";
+ post_data = "";
+ httpWrite(uri, post_data);
+
+ Log.e(TAG, "SHOW");
+
+ return false;
+ }
+
+ // The function is executed after every "Ok!" response
+ // obtained from esp32, which means a previous command
+ // is complete and esp32 is ready to get the new one.
+ //-----------------------------------------------------
+ public boolean handle_uploading_9in69b(String imgBWStr, String imgRStr) throws IOException {
+ StringBuilder imgBWStr_m1s1 = new StringBuilder();
+ StringBuilder imgRStr_m1s1 = new StringBuilder();
+
+ int i = 0;
+ int start_index = 0;
+ int end_index = 0;
+
+ int epd_width = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width;
+ int epd_height = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height;
+ int bytes_per_line = epd_width * 2 / 8;
+ // 打包第一个区域
+ for (i = 0; i < epd_height; i++) {
+ start_index = i * bytes_per_line;
+ end_index = start_index + bytes_per_line / 2;
+ imgBWStr_m1s1.append(imgBWStr.substring(start_index, end_index));
+ imgRStr_m1s1.append(imgRStr.substring(start_index, end_index));
+ }
+ // 打包第二个区域
+ for (i = 0; i < epd_height; i++) {
+ start_index = i * bytes_per_line + bytes_per_line / 2;
+ end_index = start_index + bytes_per_line / 2;
+ imgBWStr_m1s1.append(imgBWStr.substring(start_index, end_index));
+ imgRStr_m1s1.append(imgRStr.substring(start_index, end_index));
+ }
+
+ Log.e(TAG, "package for 9in69b");
+
+ int bw_data_size = imgBWStr_m1s1.length();
+ int r_data_size = imgRStr_m1s1.length();
+
+ Log.e(TAG, "bw_data_size=" + bw_data_size + ", r_data_size=" + r_data_size);
+
+ int batch_size = 30000;
+ String uri = "";
+ String post_data = "";
+ int k1 = 0;
+ int k2 = 50;
+ start_index = 0;
+ // 将第一阶段数据进行分批发送
+ while (start_index < bw_data_size) {
+ uri = "LOADA";
+ end_index = Math.min((start_index + batch_size), bw_data_size);
+ post_data = imgBWStr_m1s1.substring(start_index, end_index);
+ Log.e(TAG, "LOADA: " + start_index + "->" + end_index);
+ send_data(k1, k2, bw_data_size, end_index,uri, post_data);
+
+ start_index += batch_size;
+ }
+
+ Log.e(TAG, "LoadA finish");
+
+ k1 = 50;
+ k2 = 50;
+ start_index = 0;
+ // 再将第二阶段数据也分批发送
+ while (start_index < r_data_size) {
+ uri = "LOADB";
+ end_index = Math.min((start_index + batch_size), r_data_size);
+ post_data = imgRStr_m1s1.substring(start_index, end_index);
+ Log.e(TAG, "LOADB: " + start_index + "->" + end_index);
+ send_data(k1, k2, r_data_size, end_index,uri, post_data);
+
+ start_index += batch_size;
+ }
+ Log.e(TAG, "LoadB finish");
+
+ // 最后刷新显示
+ uri = "SHOW";
+ post_data = "";
+ httpWrite(uri, post_data);
+
+ Log.e(TAG, "SHOW");
+
+ return false;
+ }
+
+ public boolean handleUploadingStage() throws IOException {
+ StringBuilder imgBWStr = new StringBuilder();
+ StringBuilder imgRStr = new StringBuilder();
+
+ Log.e(TAG, "handleUploadingStage start");
+
+ boolean redIsEnabled = (EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].index & 1) != 0;
+ if (EPaperDisplay.epdInd == 45) {
+ redIsEnabled = false;
+ } else if (EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].index > 5) {
+ redIsEnabled = false;
+ }
+ // 根据像素信息,转换成待发送的字符串
+ while ((pxInd < array.length))
+ {
+ int v_bw = 0;
+ int v_r = 0;
+ int c_bw = 0;
+ int c_r = 3;
+
+ int epdInd = EPaperDisplay.epdInd;
+
+ // 根据像素值占用bit划分
+ if ((epdInd>15 && epdInd < 22) || (epdInd == 45)) {
+ // 像素值0~3,占用2个bit
+ for (int i = 0; i < 8; i += 2) {
+ if (pxInd < array.length) v_bw |= (array[pxInd] << (6-i));
+ pxInd++;
+ }
+ imgBWStr.append(byteToStr(v_bw));
+ } else if (epdInd == 25 || epdInd == 37 || epdInd == 43 || epdInd == 47) {
+ // 像素值0~6,占用3个bit,实际占用4bit
+ for (int i = 0; i < 8; i += 4) {
+ if (pxInd < array.length) v_bw |= (array[pxInd] << (4-i));
+ pxInd++;
+ }
+ imgBWStr.append(byteToStr(v_bw));
+ } else {
+ // 像素值0~1,占用1个bit
+ // 这里的编码采用顺序方式,即bit_[x]代表像素[x]
+ // 先用8->1的方式,将连续的8个像素压缩到一个数据字节里
+ for (int i = 0; i < 8; i++)
+ {
+ // 0x80 >> i: 0~7-> 0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0
+ if ((pxInd < array.length) && (array[pxInd] != c_bw)) v_bw |= (128 >> i);
+ if (redIsEnabled) {
+ if ((pxInd < array.length) && (array[pxInd] != c_r)) v_r |= (128 >> i);
+ }
+
+ pxInd++;
+ }
+
+ // 然后再将字节转换成字符串,压缩比为1->2
+ // 例如0xfe -> 'f' + 'e'
+ imgBWStr.append(byteToStr(v_bw));
+ if (redIsEnabled) {
+ imgRStr.append(byteToStr(v_r));
+ }
+ }
+ }
+
+ Log.e(TAG, "-> imgBWStr:" + imgBWStr.length() + ", imgRStr:" + imgRStr.length());
+
+ Log.e(TAG, String.format("%s", imgBWStr.toString().substring(0, 10)));
+
+ // 对12.48屏进行特殊处理
+ if (EPaperDisplay.epdInd == 44) {
+ return handle_uploading_12in48b(imgBWStr.toString(), imgRStr.toString());
+ } else if (EPaperDisplay.epdInd == 46) {
+ return handle_uploading_9in69b(imgBWStr.toString(), imgRStr.toString());
+ }
+
+ Log.e(TAG, "array size=" + array.length);
+ Log.e(TAG, "imgStr len=" + imgBWStr.length());
+
+ int start_index = 0;
+ int end_index = 0;
+ int bw_data_size = imgBWStr.length();
+ int r_data_size = imgRStr.length();
+
+ String uri = "";
+ String post_data = "";
+
+ int k1, k2;
+ if (redIsEnabled) {
+ k1 = 0;
+ k2 = 50;
+ } else {
+ k1 = 0;
+ k2 = 100;
+ }
+
+ // 一次性发送的话,如果数据量太大,反而效率有所降低
+ int batch_size = 30000;
+
+ start_index = 0;
+ while (start_index < bw_data_size) {
+ uri = "LOADA";
+ end_index = Math.min((start_index + batch_size), bw_data_size);
+ post_data = imgBWStr.substring(start_index, end_index);
+ send_data(k1, k2, bw_data_size, end_index,uri, post_data);
+
+ start_index += batch_size;
+ }
+
+ // 如果还有Red数据
+ if (redIsEnabled) {
+ k1 = 50;
+ k2 = 50;
+
+ // 再批量发送
+ uri = "LOADB";
+ start_index = 0;
+ while (start_index < r_data_size) {
+ end_index = Math.min((start_index + batch_size), r_data_size);
+ post_data = imgRStr.substring(start_index, end_index);
+ send_data(k1, k2, r_data_size, end_index, uri, post_data);
+ start_index += batch_size;
+ }
+ }
+
+ // 最后刷新显示
+ uri = "SHOW";
+ post_data = "";
+ httpWrite(uri, post_data);
+
+ Log.e(TAG, "handleUploadingStage end");
+
+ return false;
+ }
+
+ // Returns the index of color in palette
+ //-----------------------------------------------------
+ public int getVal(int color)
+ {
+ int r = Color.red(color);
+ int g = Color.green(color);
+ int b = Color.blue(color);
+
+// 黑色:对应00b
+// 白色:对应01b
+// 黄色:对应10b
+// 红色:对应11b
+// @ColorInt public static final int BLACK = 0xFF000000;
+// @ColorInt public static final int WHITE = 0xFFFFFFFF;
+// @ColorInt public static final int RED = 0xFFFF0000;
+// @ColorInt public static final int YELLOW = 0xFFFFFF00;
+ if((r == 0xFF) && (b == 0xFF)) return 1;
+ if((r == 0x7F) && (b == 0x7F)) return 2;
+ if((r == 0xFF) && (g == 0xFF)) return 2;
+ if((r == 0xFF) && (b == 0x00)) return 3;
+
+ return 0;
+ }
+
+ // Returns the index of color in palette just for 5.65f e-Paper
+ //-----------------------------------------------------
+ public int getVal_7color(int color)
+ {
+ int r = Color.red(color);
+ int g = Color.green(color);
+ int b = Color.blue(color);
+
+ if((r == 0x00) && (g == 0x00) && (b == 0x00)) return 0;
+ if((r == 0xFF) && (g == 0xFF) && (b == 0xFF)) return 1;
+ if((r == 0x00) && (g == 0xFF) && (b == 0x00)) return 2;
+ if((r == 0x00) && (g == 0x00) && (b == 0xFF)) return 3;
+ if((r == 0xFF) && (g == 0x00) && (b == 0x00)) return 4;
+ if((r == 0xFF) && (g == 0xFF) && (b == 0x00)) return 5;
+ if((r == 0xFF) && (g == 0x80) && (b == 0x00)) return 6;
+
+ return 7;
+ }
+
+ public int getVal_new7color(int color)
+ {
+ int r = Color.red(color);
+ int g = Color.green(color);
+ int b = Color.blue(color);
+
+ //#define EPD_7IN3E_BLACK 0x0 /// 000 0xFF000000
+ //#define EPD_7IN3E_WHITE 0x1 /// 001 0xFFFFFFFF
+ //#define EPD_7IN3E_YELLOW 0x2 /// 010 0xFFFFFF00
+ //#define EPD_7IN3E_RED 0x3 /// 011 0xFFFF0000
+ //#define EPD_7IN3E_ORANGE 0x4 /// 100
+ //#define EPD_7IN3E_BLUE 0x5 /// 101 0xFF0000FF
+ //#define EPD_7IN3E_GREEN 0x6 /// 110 0xFF00FF00
+ if((r == 0x00) && (g == 0x00) && (b == 0x00)) return 0;
+ if((r == 0xFF) && (g == 0xFF) && (b == 0xFF)) return 1;
+ if((r == 0xFF) && (g == 0xFF) && (b == 0x00)) return 2;
+ if((r == 0xFF) && (g == 0x00) && (b == 0x00)) return 3;
+ if((r == 0xFF) && (g == 0x80) && (b == 0x00)) return 4;
+ if((r == 0x00) && (g == 0x00) && (b == 0xFF)) return 5;
+ if((r == 0x00) && (g == 0xFF) && (b == 0x00)) return 6;
+
+ return 7;
+ }
+
+ // 高4位 + 低4位
+ private String byteToStr(int v) {
+ return String.valueOf((char)(((v >> 4) & 0xF) + 97)) + String.valueOf((char)((v & 0xF) + 97));
+ }
+
+ public void httpWrite(String uri, String post_data) throws IOException {
+ Log.e(TAG, "ip_address: " + mEsp32IpAddress + ", uri: " + uri);
+
+ URL url = new URL("http://" + mEsp32IpAddress + "/" + uri);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setDoOutput(true);
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Content-Type", "application/json");
+
+ OutputStream os = conn.getOutputStream();
+
+ byte[] input = post_data.getBytes("utf-8");
+ os.write(input, 0, input.length);
+
+ try {
+ InputStream is = conn.getInputStream();
+ } catch (IOException e) {
+ Log.e(TAG, "get input instream failed.");
+ }
+
+ os.close();
+ conn.disconnect();
+ }
+}
diff --git a/app/src/main/java/com/example/epaperuploader/SpiListAdapter.java b/app/src/main/java/com/example/epaperuploader/SpiListAdapter.java
new file mode 100644
index 0000000..ef00340
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/SpiListAdapter.java
@@ -0,0 +1,87 @@
+package com.example.epaperuploader;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+//---------------------------------------------------------
+// File list adapter
+//---------------------------------------------------------
+public class SpiListAdapter extends BaseAdapter
+{
+ private ArrayList mConfigInfoList;
+ private LayoutInflater mInflator;
+
+ public SpiListAdapter(Context context)
+ {
+ super();
+ mConfigInfoList = new ArrayList<>();
+ mInflator = LayoutInflater.from(context);
+ }
+
+ public void add(SpiPinConfigInfo configInfo) {
+ mConfigInfoList.add(configInfo);
+ }
+
+ public void del(int i) {
+ mConfigInfoList.remove(i);
+ }
+
+ public void clear() {
+ mConfigInfoList.clear();
+ }
+
+ @Override
+ public int getCount() {
+ return mConfigInfoList.size();
+ }
+
+ @Override
+ public SpiPinConfigInfo getItem(int i) {
+ return mConfigInfoList.get(i);
+ }
+
+ @Override
+ public long getItemId(int i) {
+ return i;
+ }
+
+ public ArrayList getList() {
+ return mConfigInfoList;
+ }
+
+ @Override
+ public View getView(int i, View view, ViewGroup viewGroup) {
+ ViewHolder viewHolder;
+ // General ListView optimization code.
+ if (view == null) {
+ view = mInflator.inflate(R.layout.spi_list_item, viewGroup, false);
+ viewHolder = new ViewHolder();
+ viewHolder.config_name = view.findViewById(R.id.config_name);
+ viewHolder.config_verbose = view.findViewById(R.id.config_verbose);
+ viewHolder.config_select = view.findViewById(R.id.config_select);
+ view.setTag(viewHolder);
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
+ }
+
+ SpiPinConfigInfo configInfo = mConfigInfoList.get(i);
+ viewHolder.config_name.setText(configInfo.getConfigName());
+ viewHolder.config_verbose.setText(configInfo.getConfigVerbose());
+ viewHolder.config_select.setChecked(configInfo.getSelectStatus());
+
+ return view;
+ }
+
+ private class ViewHolder {
+ TextView config_name;
+ TextView config_verbose;
+ RadioButton config_select;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/epaperuploader/SpiPinConfigActivity.java b/app/src/main/java/com/example/epaperuploader/SpiPinConfigActivity.java
new file mode 100644
index 0000000..e2b6614
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/SpiPinConfigActivity.java
@@ -0,0 +1,218 @@
+package com.example.epaperuploader;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.gson.Gson;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+
+public class SpiPinConfigActivity extends AppCompatActivity {
+ // View of displays list
+ //---------------------------------------------------------
+ private static ListView mSpiList;
+
+ private static SpiListAdapter mSpiListAdapter = null;
+
+ public static final int SPI_PIN_EDIT = 2;
+
+ static SharedPreferences sharedPreferences = null;
+ static SharedPreferences.Editor config_editor;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_spi_pin_config);
+
+ getSupportActionBar().setTitle("管脚配置选择 & 添加");
+
+ sharedPreferences = getSharedPreferences("config", Context.MODE_PRIVATE);
+ config_editor = sharedPreferences.edit();
+
+ mSpiListAdapter = new SpiListAdapter(this);
+ mSpiList = findViewById(R.id.spi_list);
+ mSpiList.setAdapter(mSpiListAdapter);
+
+ mSpiList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
+ set_config_select(i);
+ }
+ });
+
+ recover_spi_list();
+
+ registerForContextMenu(mSpiList);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ getMenuInflater().inflate(R.menu.config_menu_sub, menu);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo menuInfo;
+ menuInfo = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
+ if (item.getItemId() == R.id.config_menu_delete) {
+ int item_id = menuInfo.position;
+ if (item_id == 0) {
+ Toast.makeText(this, "失败:默认配置不可删除", Toast.LENGTH_SHORT).show();
+ } else {
+ del_spi_config(item_id);
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ private void recover_spi_list() {
+ String config_list_str = sharedPreferences.getString("config_list", "");
+ // 如果没有配置过SPI
+ if (config_list_str.isEmpty()) {
+ // 则下发默认配置
+ // sck=13, din=14, cs=15, busy=25, rst=26, dc=27
+ SpiPinConfigInfo configInfo = new SpiPinConfigInfo("默认配置",
+ 14, 13, 15, 27, 26, 25, false);
+ add_spi_config(configInfo);
+ update_config_list(true);
+ } else {
+ try {
+ JSONArray jsonArray = new JSONArray(config_list_str);
+ JSONObject jsonObject;
+ for (int i = 0; i < jsonArray.length(); i++) {
+ jsonObject = jsonArray.getJSONObject(i);
+ String config_name = jsonObject.optString("config_name");
+ int din = jsonObject.getInt("din");
+ int sck = jsonObject.getInt("sck");
+ int cs = jsonObject.getInt("cs");
+ int dc = jsonObject.getInt("dc");
+ int rst = jsonObject.getInt("rst");
+ int busy = jsonObject.getInt("busy");
+ boolean is_select = jsonObject.getBoolean("is_select");
+ System.out.println(config_name + ":" + "din=" + din + ", sck=" + sck + ", cs="
+ + cs + ", dc=" + dc + ", rst=" + rst + ", busy=" + busy + ", is_select=" + is_select);
+ add_spi_config(new SpiPinConfigInfo(config_name, din, sck, cs, dc, rst, busy, is_select));
+ }
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static ArrayList get_spi_config_list() {
+ return mSpiListAdapter.getList();
+ }
+
+ public void add_spi_config(SpiPinConfigInfo configInfo) {
+ mSpiListAdapter.add(configInfo);
+ update_config_list(true);
+ }
+
+ public void del_spi_config(int item_id) {
+ mSpiListAdapter.del(item_id);
+ boolean has_select = false;
+ // 此处做保护性检查
+ for (int i = 0; i < mSpiListAdapter.getCount(); i ++) {
+ if (mSpiListAdapter.getItem(i).getSelectStatus()) {
+ has_select = true;
+ break;
+ }
+ }
+ if (has_select != true) {
+ mSpiListAdapter.getItem(0).setSelectStatus(true);
+ setResult(RESULT_OK, new Intent());
+ }
+ update_config_list(true);
+ }
+
+ private void set_config_select(int select_id) {
+ for (int i = 0; i < mSpiListAdapter.getCount(); i ++) {
+ if (i == select_id) {
+ mSpiListAdapter.getItem(i).setSelectStatus(true);
+ } else {
+ mSpiListAdapter.getItem(i).setSelectStatus(false);
+ }
+ }
+
+ update_config_list(true);
+
+ setResult(RESULT_OK, new Intent());
+ finish();
+ }
+
+ private void update_config_list(boolean has_change) {
+ // 更新表格
+ mSpiListAdapter.notifyDataSetChanged();
+
+ // 保存列表
+ if (has_change) {
+ Gson gson = new Gson();
+ String config_list = gson.toJson(mSpiListAdapter.getList());
+
+ config_editor.putString("config_list", config_list);
+ config_editor.commit();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if (requestCode == SPI_PIN_EDIT) {
+ if (resultCode == RESULT_OK) {
+ String new_config = data.getStringExtra("NEW_CONFIG");
+ System.out.println("NEW:" + new_config);
+ try {
+ JSONObject jsonObject = new JSONObject(new_config);
+
+ String config_name = jsonObject.optString("config_name");
+ int din = jsonObject.getInt("din");
+ int sck = jsonObject.getInt("sck");
+ int cs = jsonObject.getInt("cs");
+ int dc = jsonObject.getInt("dc");
+ int rst = jsonObject.getInt("rst");
+ int busy = jsonObject.getInt("busy");
+ boolean is_select = jsonObject.getBoolean("is_select");
+ System.out.println(config_name + ":" + "din=" + din + ", sck=" + sck + ", cs="
+ + cs + ", dc=" + dc + ", rst=" + rst + ", busy=" + busy + ", is_select=" + is_select);
+ add_spi_config(new SpiPinConfigInfo(config_name, din, sck, cs, dc, rst, busy, is_select));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ public void addSpiPinConfig(View view) {
+ startActivityForResult(
+ new Intent(this, SpiPinEditActivity.class),
+ SPI_PIN_EDIT);
+ }
+
+ public static SpiPinConfigInfo getSpiConfig() {
+ SpiPinConfigInfo configInfo;
+ for (int i = 0; i < mSpiListAdapter.getCount(); i ++) {
+ configInfo = mSpiListAdapter.getItem(i);
+ if (configInfo.getSelectStatus()) {
+ return configInfo;
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/epaperuploader/SpiPinConfigInfo.java b/app/src/main/java/com/example/epaperuploader/SpiPinConfigInfo.java
new file mode 100644
index 0000000..470eb9a
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/SpiPinConfigInfo.java
@@ -0,0 +1,37 @@
+package com.example.epaperuploader;
+
+//sck=13, din=14, cs=15, busy=25, rst=26, dc=27
+public class SpiPinConfigInfo {
+ public String config_name;
+ private int din;
+ private int sck;
+ private int cs;
+ private int dc;
+ private int rst;
+ private int busy;
+ private boolean is_select;
+
+ public SpiPinConfigInfo(
+ String config_name,
+ int din, int sck, int cs, int dc, int rst, int busy, boolean is_select ) {
+ this.config_name = config_name;
+ this.din = din;
+ this.sck = sck;
+ this.cs = cs;
+ this.dc = dc;
+ this.rst = rst;
+ this.busy = busy;
+ this.is_select = is_select;
+ }
+
+ public String getConfigName() {
+ return config_name;
+ }
+ public boolean getSelectStatus() { return is_select; }
+ public void setSelectStatus(boolean select_status) { is_select = select_status; }
+
+ //sck=13, din=14, cs=15, busy=25, rst=26, dc=27
+ public String getConfigVerbose() {
+ return "din=" + din + ", sck=" + sck + ", cs=" + cs + ", dc=" + dc + ", rst=" + rst + ", busy=" + busy;
+ }
+}
diff --git a/app/src/main/java/com/example/epaperuploader/SpiPinEditActivity.java b/app/src/main/java/com/example/epaperuploader/SpiPinEditActivity.java
new file mode 100644
index 0000000..eb27ee6
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/SpiPinEditActivity.java
@@ -0,0 +1,87 @@
+package com.example.epaperuploader;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.gson.Gson;
+
+import java.util.ArrayList;
+
+public class SpiPinEditActivity extends AppCompatActivity {
+
+ private EditText mConfigName;
+ private EditText mCsPin;
+ private EditText mDcPin;
+ private EditText mDinPin;
+ private EditText mSckPin;
+ private EditText mRstPin;
+ private EditText mBusyPin;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_spi_pin_edit);
+
+ getSupportActionBar().setTitle("配置编辑");
+
+ mConfigName = findViewById(R.id.edit_config_name);
+ mCsPin = findViewById(R.id.edit_cs_pin);
+ mDcPin = findViewById(R.id.edit_dc_pin);
+ mDinPin = findViewById(R.id.edit_din_pin);
+ mSckPin = findViewById(R.id.edit_sck_pin);
+ mRstPin = findViewById(R.id.edit_rst_pin);
+ mBusyPin = findViewById(R.id.edit_busy_pin);
+ }
+
+ SpiPinConfigInfo checkEditConfig() {
+ String config_name = mConfigName.getText().toString().trim();
+ String din_pin = mDinPin.getText().toString().trim();
+ String sck_pin = mSckPin.getText().toString().trim();
+ String cs_pin = mCsPin.getText().toString().trim();
+ String dc_pin = mDcPin.getText().toString().trim();
+ String rst_pin = mRstPin.getText().toString().trim();
+ String busy_pin = mBusyPin.getText().toString().trim();
+ if (config_name.isEmpty()
+ || din_pin.isEmpty()
+ || sck_pin.isEmpty()
+ || cs_pin.isEmpty()
+ || dc_pin.isEmpty()
+ || rst_pin.isEmpty()
+ || busy_pin.isEmpty()) {
+ Toast.makeText(this, "错误:以上填写字段不允许为空", Toast.LENGTH_SHORT).show();
+ return null;
+ } else {
+ ArrayList spi_config_list = SpiPinConfigActivity.get_spi_config_list();
+ for (int i = 0; i < spi_config_list.size(); i++) {
+ if (config_name.equals(spi_config_list.get(i).getConfigName())) {
+ Toast.makeText(this, "错误:与已保存的配置名称冲突", Toast.LENGTH_SHORT).show();
+ return null;
+ }
+ }
+ SpiPinConfigInfo configInfo = new SpiPinConfigInfo(config_name,
+ Integer.valueOf(din_pin), Integer.valueOf(sck_pin), Integer.valueOf(cs_pin),
+ Integer.valueOf(dc_pin), Integer.valueOf(rst_pin), Integer.valueOf(busy_pin),
+ false
+ );
+ return configInfo;
+ }
+ }
+
+ public void saveEditConfig(View view) {
+ SpiPinConfigInfo configInfo = checkEditConfig();
+ if (configInfo == null) {
+ return;
+ }
+ Toast.makeText(this, "配置检查通过", Toast.LENGTH_SHORT).show();
+ Intent intent = new Intent();
+ Gson gson = new Gson();
+ intent.putExtra("NEW_CONFIG", gson.toJson(configInfo));
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/epaperuploader/communication/PermissionHelper.java b/app/src/main/java/com/example/epaperuploader/communication/PermissionHelper.java
new file mode 100644
index 0000000..efd5119
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/communication/PermissionHelper.java
@@ -0,0 +1,173 @@
+package com.example.epaperuploader.communication;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+
+import com.example.epaperuploader.R;
+
+/**
+ *
Permission helper
+ * The class provides functions for delayed operations,
+ * which require permissions and interaction with user.
+ *
+ * @author Waveshare team
+ * @version 1.0
+ * @since 8/16/2018
+ */
+
+public class PermissionHelper implements DialogInterface.OnDismissListener
+{
+ // Permissions used in the application
+ //---------------------------------------------------------
+ public static final int REQ_BLUE = 0; // For bluetooth devices scanning
+ public static final int REQ_READ = 1; // For reading from external storage
+
+ // Permissions related messages
+ //---------------------------------------------------------
+ private static final String[] messages = new String[]
+ {
+ "Please accept the GPS Location permission",
+ "Please accept the Storage reading permission",
+ "The GPS Location permission is denied",
+ "The Storage Reading permission is denied",
+ };
+
+ // Full names of permissions
+ //---------------------------------------------------------
+ private static final String[] permissions = new String[]
+ {
+ Manifest.permission.ACCESS_COARSE_LOCATION,
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ };
+
+ // Wrapped names of permissions wrapped (used in requests)
+ //---------------------------------------------------------
+ private static final String[][] permissionArrays = new String[][]
+ {
+ new String[] { Manifest.permission.ACCESS_COARSE_LOCATION },
+ new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
+ };
+
+ // Granted permission event handler interface
+ //---------------------------------------------------------
+ public interface PermissionResponse
+ {
+ void invoke();
+ }
+
+ // Array of granted permission event handlers
+ //---------------------------------------------------------
+ private static PermissionResponse[] responses = new PermissionResponse[]
+ {
+ null,
+ null,
+ };
+
+ private Activity context; // Activity which needs a permission
+ private int reqCode; //
+
+ public PermissionHelper(Activity activity)
+ {
+ context = activity;
+ }
+
+ public AlertDialog show(int requestCode)
+ {
+ reqCode = requestCode;
+ AlertDialog.Builder dialog = new AlertDialog.Builder(context);
+
+ dialog.setTitle(R.string.dlg_note);
+ dialog.setOnDismissListener(this);
+ dialog.setPositiveButton(R.string.btn_okey, null);
+ dialog.setMessage(messages[reqCode]);
+
+ return dialog.show();
+ }
+
+ public void onDismiss(DialogInterface dialog)
+ {
+ if (reqCode >= permissions.length) return;
+ ActivityCompat.requestPermissions(context, permissionArrays[reqCode], reqCode);
+ }
+
+ // Interaction with user and sending the request permission
+ //---------------------------------------------------------
+ public boolean sendRequestPermission(int requestCode)
+ {
+ // Get permission if possible
+ //-----------------------------------------------------
+ if (requestCode >= permissions.length) return false;
+ String permission = permissions[requestCode];
+
+ // Return true if it is already granted
+ //-----------------------------------------------------
+ if (context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED)
+ return true;
+
+ // Interact with user if it's needed
+ //-----------------------------------------------------
+ if (context.shouldShowRequestPermissionRationale(permission))
+ show(requestCode);
+
+ // Request the permission
+ //-----------------------------------------------------
+ else context.requestPermissions(permissionArrays[requestCode], requestCode);
+
+ return false;
+ }
+
+ // Getting and handling the request permission result
+ //---------------------------------------------------------
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)
+ {
+ // Unknown result
+ //-----------------------------------------------------
+ if ((requestCode < 0) || (requestCode >= permissions.length))
+ return;
+
+ // Notify that permission is not granted yet
+ //-----------------------------------------------------
+ if (context.checkSelfPermission(permissions[requestCode]) != PackageManager.PERMISSION_GRANTED)
+ {
+ if (responses[requestCode] != null)
+ show(requestCode + permissions.length);
+ }
+
+ // Perform granted permission's response function
+ //-----------------------------------------------------
+ else if (responses[requestCode] != null)
+ responses[requestCode].invoke();
+
+ // Clear corresponded cell in responses array
+ //-----------------------------------------------------
+ responses[requestCode] = null;
+ }
+
+ // Set response on
+ //---------------------------------------------------------
+ public void setResponse(int requestCode, PermissionResponse response)
+ {
+ if ((requestCode < 0) || (requestCode >= responses.length)) return;
+ responses[requestCode] = response;
+ }
+
+ // Notification dialog
+ //---------------------------------------------------------
+ public static void note(Activity activity, int msg)
+ {
+ Resources resources = activity.getResources();
+ AlertDialog.Builder dialog = new AlertDialog.Builder(activity);
+
+ dialog.setTitle(resources.getString(R.string.dlg_note));
+ dialog.setPositiveButton(resources.getString(R.string.btn_okey), null);
+ dialog.setMessage(resources.getString(msg));
+ dialog.show();
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/epaperuploader/image_processing/EPaperDisplay.java b/app/src/main/java/com/example/epaperuploader/image_processing/EPaperDisplay.java
new file mode 100644
index 0000000..576da87
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/image_processing/EPaperDisplay.java
@@ -0,0 +1,104 @@
+package com.example.epaperuploader.image_processing;
+
+/**
+ * Created by YiWan on 8/24/2018.
+ */
+
+/**
+ *
e-Paper display characteristics
+ * The class is intended to store width, height and index of palette.
+ *
+ * @author Waveshare team
+ * @version 1.0
+ * @since 8/14/2018
+ */
+
+public class EPaperDisplay
+{
+ public int width;
+ public int height;
+ public int index;
+ public String title;
+
+ /**
+ * Stores display's characteristics.
+ * @param width of display in pixels
+ * @param height of display on pixels
+ * @param index of palette
+ * @param title of display
+ */
+ public EPaperDisplay(int width, int height, int index, String title)
+ {
+ this.width = width;
+ this.height = height;
+ this.index = index;
+ this.title = title;
+ }
+
+ // Index of selected display
+ //---------------------------------------------------------
+ public static int epdInd = -1;
+
+ // Array of display characteristics
+ //---------------------------------------------------------
+ private static EPaperDisplay[] array = null;
+
+ public static EPaperDisplay[] getDisplays()
+ {
+ if (array == null)
+ {
+ array = new EPaperDisplay[]
+ {
+ new EPaperDisplay(200,200,0, "1.54 inch e-Paper"), // 0
+ new EPaperDisplay(200,200,3, "1.54 inch e-Paper (B)"), // 1
+ new EPaperDisplay(152,152,5, "1.54 inch e-Paper (C)"), // 2
+ new EPaperDisplay(122,250,0, "2.13 inch e-Paper"), // 3
+ new EPaperDisplay(104,212,1, "2.13 inch e-Paper (B)"), // 4
+ new EPaperDisplay(104,212,5, "2.13 inch e-Paper (C)"), // 5
+ new EPaperDisplay(104,212,0, "2.13 inch e-Paper (D)"), // 6
+ new EPaperDisplay(176,264,0, "2.7 inch e-Paper"), // 7
+ new EPaperDisplay(176,264,1, "2.7 inch e-Paper (B)"), // 8
+ new EPaperDisplay(128,296,0, "2.9 inch e-Paper"), // 9
+ new EPaperDisplay(128,296,1, "2.9 inch e-Paper (B)"), // 10
+ new EPaperDisplay(128,296,5, "2.9 inch e-Paper (C)"), // 11
+ new EPaperDisplay(128,296,0, "2.9 inch e-Paper (D)"), // 12
+ new EPaperDisplay(400,300,0, "4.2 inch e-Paper"), // 13
+ new EPaperDisplay(400,300,1, "4.2 inch e-Paper (B)"), // 14
+ new EPaperDisplay(400,300,5, "4.2 inch e-Paper (C)"), // 15
+ new EPaperDisplay(600,448,0, "5.83 inch e-Paper"), // 16
+ new EPaperDisplay(600,448,1, "5.83 inch e-Paper (B)"), // 17
+ new EPaperDisplay(600,448,5, "5.83 inch e-Paper (C)"), // 18
+ new EPaperDisplay(640,384,0, "7.5 inch e-Paper"), // 19
+ new EPaperDisplay(640,384,1, "7.5 inch e-Paper (B)"), // 20
+ new EPaperDisplay(640,384,5, "7.5 inch e-Paper (C)"), // 21
+ new EPaperDisplay(800,480,0, "7.5 inch e-Paper V2"), // 22
+ new EPaperDisplay(800,480,1, "7.5 inch e-Paper (B) V2"), // 23
+ new EPaperDisplay(880,528,1, "7.5 inch HD e-Paper (B)"), // 24
+ new EPaperDisplay(600,448,7, "5.65 inch e-Paper (F)"), // 25
+ new EPaperDisplay(880,528,0, "7.5 inch HD e-Paper"), // 26
+ new EPaperDisplay(280,480,0, "3.7 inch e-Paper"), // 27
+ new EPaperDisplay(152,296,0, "2.66 inch e-Paper"), // 28
+ new EPaperDisplay(648,480,1, "5.83 inch e-Paper (B) V2"), // 29
+ new EPaperDisplay(128,296,1, "2.9 inch e-Paper (B) V3"), // 30
+ new EPaperDisplay(200,200,1, "1.54 inch e-Paper (B) V2"), // 31
+ new EPaperDisplay(104,214,1, "2.13 inch e-Paper (B) V3"), // 32
+ new EPaperDisplay(128,296,0, "2.9 inch e-Paper V2"), // 33
+ new EPaperDisplay(400,300,1, "4.2 inch e-Paper (B) V2"), // 34
+ new EPaperDisplay(152,296,1, "2.66 inch e-Paper (B)"), // 35
+ new EPaperDisplay(648,480,0, "5.83 inch e-Paper V2"), // 36
+ new EPaperDisplay(640,400,7, "4.01 inch e-Paper (F)"), // 37
+ new EPaperDisplay(176,264,1, "2.7 inch e-Paper (B) V2"), // 38
+ new EPaperDisplay(122,250,0, "2.13 inch e-Paper V3"), // 39
+ new EPaperDisplay(122,250,1, "2.13 inch e-Paper (B) V4"), // 40
+ new EPaperDisplay(240,360,0, "3.52 inch e-Paper"), // 41
+ new EPaperDisplay(176,264,0, "2.7 inch e-Paper V2"), // 42
+ new EPaperDisplay(800,480,7, "7.3 inch e-Paper (F)"), // 43
+ new EPaperDisplay(1304,984,1, "12.48 inch e-Paper (B) V1"), // 44
+ new EPaperDisplay(960,672,8, "9.7 inch e-Paper (G)"), // 45
+ new EPaperDisplay(960,672,1, "9.69 inch e-Paper (B)"), // 46
+ new EPaperDisplay(800,480,9, "7.3 inch e-Paper (E)"), // 47
+ }; // index & 0x1 != 0 代表具有彩色
+ }
+ return array;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/example/epaperuploader/image_processing/EPaperPicture.java b/app/src/main/java/com/example/epaperuploader/image_processing/EPaperPicture.java
new file mode 100644
index 0000000..d17920e
--- /dev/null
+++ b/app/src/main/java/com/example/epaperuploader/image_processing/EPaperPicture.java
@@ -0,0 +1,232 @@
+package com.example.epaperuploader.image_processing;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+
+import com.example.epaperuploader.MainActivity;
+
+/**
+ *
Image filtering for e-Paper display
+ * The class is intended for pixel data converting
+ * from ARGB format to e-Paper display suitable format.
+ * The result of the pixel format's converting is an index
+ * of color in e-Paper dislay's palette
+ *
+ * @author Waveshare team
+ * @version 1.0
+ * @since 8/14/2018
+ */
+
+public class EPaperPicture
+{
+ private static int srcW, srcH; // Width and height of source image
+ private static int dstW, dstH; // Width and height of destination image
+
+ private static Bitmap srcBmp; // Bitmap of source image
+ private static Bitmap dstBmp; // Bitmap of destination image
+
+ private static int[] curPal; // Current palette
+
+ private static boolean isImageEnhance;
+
+ private static int[][] palettes = new int[][] // Palettes
+ {
+ new int[] { Color.BLACK, Color.WHITE },
+ new int[] { Color.BLACK, Color.WHITE, Color.RED },
+ new int[] { Color.BLACK, Color.WHITE, Color.GRAY },
+ new int[] { Color.BLACK, Color.WHITE, Color.GRAY, Color.RED },
+ new int[] { Color.BLACK, Color.WHITE },
+ new int[] { Color.BLACK, Color.WHITE, Color.YELLOW },
+ new int[] {},
+ new int[] { Color.BLACK, Color.WHITE, Color.GREEN, Color.BLUE, Color.RED, Color.YELLOW, 0xffff8000 /* orange */},
+ new int[] { Color.BLACK, Color.WHITE, Color.YELLOW, Color.RED },
+ new int[] { Color.BLACK, Color.WHITE, Color.YELLOW, Color.RED, 0xffff8000, Color.BLUE, Color.GREEN },
+ };
+
+ // Return the square error of {r, g, b},
+ // that means how far them are from standard color stdCol
+ //---------------------------------------------------------
+ private static double getErr(double r, double g, double b, int stdCol)
+ {
+ r -= Color.red (stdCol);
+ g -= Color.green(stdCol);
+ b -= Color.blue (stdCol);
+
+ return r*r + g*g + b*b;
+ }
+
+ // Return the index of current palette color which is
+ // nearest to the {r, g, b}
+ //---------------------------------------------------------
+ private static int getNear(double r, double g, double b)
+ {
+ int ind = 0;
+ double err = getErr(r, g, b, curPal[0]);
+
+ for (int i = 1; i < curPal.length; i++)
+ {
+ double cur = getErr(r, g, b, curPal[i]);
+ if (cur < err) { err = cur; ind = i; }
+ }
+
+ return ind;
+ }
+
+ // Return the index of current palette color which is
+ // nearest to the color clr
+ //---------------------------------------------------------
+ private static int getNear(int clr)
+ {
+ return getNear(Color.red(clr), Color.green(clr), Color.blue(clr));
+ }
+
+ // Adding of color {r, g, b} into e color array with
+ // weight k. Here every r, g or b channel takes one cell
+ // in e color array and can have any integer value.
+ //---------------------------------------------------------
+ private static void addVal(double[] e, int i, double r, double g, double b, double k)
+ {
+ int factor = isImageEnhance ? 32 : 16;
+ int index = i * 3;
+ e[index ] = (r * k) / factor + e[index ];
+ e[index + 1] = (g * k) / factor + e[index + 1];
+ e[index + 2] = (b * k) / factor + e[index + 2];
+ }
+
+ // Returns a color from the current palette
+ // which is nearest to source bitmap pixel at (x, y), or
+ // returns default color if (x, y) is out of the bitmap
+ //---------------------------------------------------------
+ private static int nearColor(int x, int y)
+ {
+ if ((x >= srcW) || (y >= srcH)) return curPal[(x + y) % 2 == 0 ? 1 : 0];
+ return curPal[getNear(srcBmp.getPixel(x, y))];
+ }
+
+ // Returns bitmap of pixels from current palette.
+ //
+ // isLvl = true - the method of searching the nearest color
+ // is based on estimation how less the original pixel
+ // differs from the palette's pixels.
+ //
+ // isLvl = false - the method of searching the nearest color
+ // is based on estimation how less the original pixel's area
+ // differs from the palette's pixels (so called dithering).
+ //---------------------------------------------------------
+ public static Bitmap createIndexedImage(boolean isLvl, boolean isRed, boolean isEnhance)
+ {
+ System.out.println("createIndexedImage is calling, " + isLvl + "," + isRed + ',' + isEnhance);
+
+ EPaperDisplay epd = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd];
+ srcBmp = MainActivity.originalImage;
+ dstBmp = Bitmap.createBitmap(epd.width, epd.height, srcBmp.getConfig());
+ isImageEnhance = isEnhance;
+
+ int palInd = epd.index;
+ if (!isRed) palInd = palInd & 0xE;
+
+ curPal = palettes[palInd];
+
+ dstW = dstBmp.getWidth();
+ dstH = dstBmp.getHeight();
+
+ srcW = srcBmp.getWidth();
+ srcH = srcBmp.getHeight();
+
+ int[] srcArr = new int[srcW * srcH];
+ int[] dstArr = new int[dstW * dstH];
+
+ int index = 0;
+ srcBmp.getPixels(srcArr, 0, srcW, 0, 0, srcW, srcH);
+
+ if (isLvl)
+ {
+ for (int y = 0; y < dstH; y++)
+ for (int x = 0; x < dstW; x++) {
+ dstArr[index++] = nearColor(x, y);
+ }
+ }
+ else
+ {
+ int aInd = 0;
+ int bInd = 1;
+
+ double[][] errArr = new double[2][];
+
+ errArr[0] = new double[3*dstW];
+ errArr[1] = new double[3*dstW];
+
+ for (int i = 0; i < dstW; i++)
+ {
+ errArr[bInd][3*i ] = 0;
+ errArr[bInd][3*i + 1] = 0;
+ errArr[bInd][3*i + 2] = 0;
+ }
+
+ for (int j = 0; j < dstH; j++)
+ {
+ if (j >= srcH)
+ {
+ for (int i = 0; i < dstW; i++, index++)
+ dstArr[index] = curPal[(i + j) % 2 == 0 ? 1 : 0];
+ continue;
+ }
+
+ aInd = ((bInd = aInd) + 1) & 1;
+
+ for (int i = 0; i < dstW; i++)
+ {
+ errArr[bInd][3*i ] = 0;
+ errArr[bInd][3*i + 1] = 0;
+ errArr[bInd][3*i + 2] = 0;
+ }
+
+ for (int i = 0; i < dstW; i++)
+ {
+ if (i >= srcW)
+ {
+ dstArr[index++] = curPal[(i + j) % 2 == 0 ? 1 : 0];
+ continue;
+ }
+
+ int srcPix = srcArr[j * srcW + i];
+
+ double r = Color.red (srcPix) + errArr[aInd][3*i ];
+ double g = Color.green(srcPix) + errArr[aInd][3*i + 1];
+ double b = Color.blue (srcPix) + errArr[aInd][3*i + 2];
+
+ int colVal = curPal[getNear(r, g, b)];
+ dstArr[index++] = colVal;
+
+ r -= Color.red (colVal);
+ g -= Color.green(colVal);
+ b -= Color.blue (colVal);
+
+ if (i == 0)
+ {
+ addVal(errArr[bInd], (i ), r, g, b, 7.0);
+ addVal(errArr[bInd], (i + 1), r, g, b, 2.0);
+ addVal(errArr[aInd], (i + 1), r, g, b, 7.0);
+ }
+ else if (i == dstW - 1)
+ {
+ addVal(errArr[bInd], (i - 1), r, g, b, 7.0);
+ addVal(errArr[bInd], (i ), r, g, b, 9.0);
+ }
+ else
+ {
+ addVal(errArr[bInd], (i - 1), r, g, b, 3.0);
+ addVal(errArr[bInd], (i ), r, g, b, 5.0);
+ addVal(errArr[bInd], (i + 1), r, g, b, 1.0);
+ addVal(errArr[aInd], (i + 1), r, g, b, 7.0);
+ }
+ }
+ }
+ }
+
+ // Put converted pixels into destination image bitmap
+ //-----------------------------------------------------
+ dstBmp.setPixels(dstArr, 0, dstW, 0, 0, dstW, dstH);
+ return dstBmp;
+ }
+}
diff --git a/app/src/main/res/drawable/bt.xml b/app/src/main/res/drawable/bt.xml
new file mode 100644
index 0000000..8087351
--- /dev/null
+++ b/app/src/main/res/drawable/bt.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/image.xml b/app/src/main/res/drawable/image.xml
new file mode 100644
index 0000000..364bf08
--- /dev/null
+++ b/app/src/main/res/drawable/image.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/pin.xml b/app/src/main/res/drawable/pin.xml
new file mode 100644
index 0000000..454e43f
--- /dev/null
+++ b/app/src/main/res/drawable/pin.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/reboot.xml b/app/src/main/res/drawable/reboot.xml
new file mode 100644
index 0000000..ffeb988
--- /dev/null
+++ b/app/src/main/res/drawable/reboot.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/right_menu.xml b/app/src/main/res/drawable/right_menu.xml
new file mode 100644
index 0000000..850600c
--- /dev/null
+++ b/app/src/main/res/drawable/right_menu.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/app/src/main/res/drawable/wifi.xml b/app/src/main/res/drawable/wifi.xml
new file mode 100644
index 0000000..660b9f1
--- /dev/null
+++ b/app/src/main/res/drawable/wifi.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_crop_image.xml b/app/src/main/res/layout/activity_crop_image.xml
new file mode 100644
index 0000000..83011c7
--- /dev/null
+++ b/app/src/main/res/layout/activity_crop_image.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_device_scan.xml b/app/src/main/res/layout/activity_device_scan.xml
new file mode 100644
index 0000000..d1570f1
--- /dev/null
+++ b/app/src/main/res/layout/activity_device_scan.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_displays.xml b/app/src/main/res/layout/activity_displays.xml
new file mode 100644
index 0000000..c2ef0c6
--- /dev/null
+++ b/app/src/main/res/layout/activity_displays.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_filtering.xml b/app/src/main/res/layout/activity_filtering.xml
new file mode 100644
index 0000000..911a7c7
--- /dev/null
+++ b/app/src/main/res/layout/activity_filtering.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
new file mode 100644
index 0000000..869c2c6
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_spi_pin_config.xml b/app/src/main/res/layout/activity_spi_pin_config.xml
new file mode 100644
index 0000000..8a0e97a
--- /dev/null
+++ b/app/src/main/res/layout/activity_spi_pin_config.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_spi_pin_edit.xml b/app/src/main/res/layout/activity_spi_pin_edit.xml
new file mode 100644
index 0000000..fa57ba5
--- /dev/null
+++ b/app/src/main/res/layout/activity_spi_pin_edit.xml
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/listitem_device.xml b/app/src/main/res/layout/listitem_device.xml
new file mode 100644
index 0000000..228fb6b
--- /dev/null
+++ b/app/src/main/res/layout/listitem_device.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/spi_list_item.xml b/app/src/main/res/layout/spi_list_item.xml
new file mode 100644
index 0000000..10fc70b
--- /dev/null
+++ b/app/src/main/res/layout/spi_list_item.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/config_menu.xml b/app/src/main/res/menu/config_menu.xml
new file mode 100644
index 0000000..95c8142
--- /dev/null
+++ b/app/src/main/res/menu/config_menu.xml
@@ -0,0 +1,22 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/config_menu_sub.xml b/app/src/main/res/menu/config_menu_sub.xml
new file mode 100644
index 0000000..eb2fdc4
--- /dev/null
+++ b/app/src/main/res/menu/config_menu_sub.xml
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/gatt_services.xml b/app/src/main/res/menu/gatt_services.xml
new file mode 100644
index 0000000..6b2ed40
--- /dev/null
+++ b/app/src/main/res/menu/gatt_services.xml
@@ -0,0 +1,25 @@
+
+
+
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
new file mode 100644
index 0000000..39dd66a
--- /dev/null
+++ b/app/src/main/res/menu/main.xml
@@ -0,0 +1,29 @@
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..67d2b14
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
new file mode 100644
index 0000000..3caeeee
--- /dev/null
+++ b/app/src/main/res/values-zh/strings.xml
@@ -0,0 +1,55 @@
+
+ 墨水屏ESP32无线图传
+ 蓝牙
+
+
+ 连接蓝牙
+ 蓝牙已连接
+ 蓝牙已断开
+ 蓝牙连接失败,请重试
+ 打开图像文件
+ 选择墨水屏型号
+ 选择图片处理算法
+ 上传图像
+ 连接到wi-fi
+ 检查Wi-Fi连接
+ 在浏览器中打开
+
+ 连接到:
+ 被装载:
+ 被选择:
+ 被卸载:
+
+ 黑白色阶算法
+ 彩色色阶算法
+ 黑白抖动算法
+ 黑白抖动算法2
+ 彩色抖动算法
+ 彩色抖动算法2
+
+ 确定
+ 返回..
+ 取消
+
+ 没有选择蓝牙设备
+ 没有选择图像文件
+ 没有选择墨水屏型号
+ 没有选择图片处理算法
+ IP没有被获取
+ 没有连接
+
+ 选择设备
+ 选择图像文件
+ 选择墨水屏型号
+ 选择图片处理算法
+ 上传
+ 警告:
+
+ Loading is failed
+ Filtering is failed
+ Uploading is failed
+
+ 等一下...
+ 连接不了
+ 秒
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..4ecce82
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+
+
+ #FF000000
+ #FFFFFFFF
+ #3F51B5
+ #303F9F
+ #FF4081
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..1fa3e24
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,74 @@
+
+ EPaperUploader
+
+ bluetooth
+
+
+ BLE is not supported
+ Data:
+ Device address:
+ State:
+ No data
+ BLE Device Scan
+ Bluetooth not supported.
+
+ Unknown device
+ Unknown characteristic
+ Unknown service
+
+
+ Connect
+ Disconnect
+ Scan
+ Stop
+
+ Bluetooth connection
+ Connected
+ Disconnected
+ Connect fail, please retry
+ Load image file
+ Select display type
+ Select image filter
+ Upload image
+ Wi-Fi connection
+ Check Wi-Fi connection
+ Open in browser
+
+ Connected to:
+ Loaded:
+ Selected:
+ Uploaded:
+
+ Level: mono
+ Level: color
+ Dithering: mono
+ Dithering: mono2
+ Dithering: color
+ Dithering: color2
+
+ Ok
+ Back..
+ Cancel
+
+ Bluetooth device is not selected
+ Image file is not loaded
+ Display type is not selected
+ Image filter is not loaded
+ IP is not defined yet
+ Connection is lost
+
+ Bluetooth devices
+ File browser
+ Select display type
+ Select image filter
+ Uploaing
+ Notification:
+
+ Loading is failed
+ Filtering is failed
+ Uploading is failed
+
+ Wait please
+ Impossible to connect
+ sec.
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..92938b5
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..5510222
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..fa0f996
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/example/epaperuploader/ExampleUnitTest.java b/app/src/test/java/com/example/epaperuploader/ExampleUnitTest.java
new file mode 100644
index 0000000..250ed38
--- /dev/null
+++ b/app/src/test/java/com/example/epaperuploader/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.example.epaperuploader;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..565f8c2
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,4 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+alias(libs.plugins.android.application) apply false
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..4387edc
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..bdc2b99
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,22 @@
+[versions]
+agp = "8.6.0"
+junit = "4.13.2"
+junitVersion = "1.1.5"
+espressoCore = "3.5.1"
+appcompat = "1.6.1"
+material = "1.10.0"
+activity = "1.8.0"
+constraintlayout = "2.1.4"
+
+[libraries]
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a40ed4b
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Mar 22 23:01:55 HKT 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..808054d
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,23 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "EPaperUploader"
+include ':app'