diff --git a/android/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460d..00000000 --- a/android/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 38eddabd..9dcb9d28 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,6 +37,8 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.google.zxing:core:3.4.1' + implementation 'com.journeyapps:zxing-android-embedded:4.2.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 15079381..dc408806 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,13 +2,19 @@ + + + + + + android:theme="@style/AppTheme" + android:hardwareAccelerated="true"> + + diff --git a/android/app/src/main/java/ipfs/gomobile/example/DisplayImageActivity.java b/android/app/src/main/java/ipfs/gomobile/example/DisplayImageActivity.java index 63160f4e..ebf047e6 100644 --- a/android/app/src/main/java/ipfs/gomobile/example/DisplayImageActivity.java +++ b/android/app/src/main/java/ipfs/gomobile/example/DisplayImageActivity.java @@ -12,6 +12,8 @@ public class DisplayImageActivity extends AppCompatActivity { private static final String TAG = "DisplayImageActivity"; + public static byte[] fetchedData; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -27,8 +29,7 @@ protected void onCreate(Bundle savedInstanceState) { } try { - byte[] data = intent.getExtras().getByteArray("ImageData"); - Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + Bitmap bitmap = BitmapFactory.decodeByteArray(fetchedData, 0, fetchedData.length); ImageView imageView = findViewById(R.id.imageView); imageView.setImageBitmap(bitmap); diff --git a/android/app/src/main/java/ipfs/gomobile/example/FetchFile.java b/android/app/src/main/java/ipfs/gomobile/example/FetchFile.java new file mode 100644 index 00000000..1ae0b904 --- /dev/null +++ b/android/app/src/main/java/ipfs/gomobile/example/FetchFile.java @@ -0,0 +1,76 @@ +package ipfs.gomobile.example; + +import android.content.Intent; +import android.os.AsyncTask; +import android.util.Log; + +import org.json.JSONObject; + +import java.lang.ref.WeakReference; +import java.util.Random; + +import ipfs.gomobile.android.IPFS; + +final class FetchFile extends AsyncTask { + private static final String TAG = "FetchIPFSFile"; + + private final WeakReference activityRef; + private boolean backgroundError; + private byte[] fetchedData; + private String cid; + + FetchFile(MainActivity activity, String cid) { + activityRef = new WeakReference<>(activity); + this.cid = cid; + } + + @Override + protected void onPreExecute() { + MainActivity activity = activityRef.get(); + if (activity == null || activity.isFinishing()) return; + + activity.displayStatusProgress(activity.getString(R.string.titleImageFetching)); + } + + @Override + protected String doInBackground(Void... v) { + MainActivity activity = activityRef.get(); + if (activity == null || activity.isFinishing()) { + cancel(true); + return null; + } + + IPFS ipfs = activity.getIpfs(); + + try { + fetchedData = ipfs.newRequest("cat") + .withArgument(cid) + .send(); + +// Log.d(TAG, "fetched file data=" + MainActivity.bytesToHex(fetchedData)); + return activity.getString(R.string.titleFetchedImage); + } catch (Exception err) { + backgroundError = true; + return MainActivity.exceptionToString(err); + } + } + + protected void onPostExecute(String result) { + MainActivity activity = activityRef.get(); + if (activity == null || activity.isFinishing()) return; + + if (backgroundError) { + activity.displayStatusError(activity.getString(R.string.titleImageFetchingErr), result); + Log.e(TAG, "Ipfs image fetch error: " + result); + } else { + activity.displayStatusSuccess(); + + // Put directly data through this way because of size limit with Intend + DisplayImageActivity.fetchedData = fetchedData; + + Intent intent = new Intent(activity, DisplayImageActivity.class); + intent.putExtra("Title", result); + activity.startActivity(intent); + } + } +} diff --git a/android/app/src/main/java/ipfs/gomobile/example/FetchRandomXKCD.java b/android/app/src/main/java/ipfs/gomobile/example/FetchRandomXKCD.java index b21bc80d..e39d2485 100644 --- a/android/app/src/main/java/ipfs/gomobile/example/FetchRandomXKCD.java +++ b/android/app/src/main/java/ipfs/gomobile/example/FetchRandomXKCD.java @@ -33,7 +33,7 @@ protected void onPreExecute() { MainActivity activity = activityRef.get(); if (activity == null || activity.isFinishing()) return; - activity.displayFetchProgress(); + activity.displayStatusProgress(activity.getString(R.string.titleXKCDFetching)); } @Override @@ -86,13 +86,15 @@ protected void onPostExecute(String result) { if (activity == null || activity.isFinishing()) return; if (backgroundError) { - activity.displayFetchError(result); + activity.displayStatusError(activity.getString(R.string.titleXKCDFetchingErr), result); Log.e(TAG, "XKCD fetch error: " + result); } else { - activity.displayFetchSuccess(); + activity.displayStatusSuccess(); + + // Put directly data through this way because of size limit with Intend + DisplayImageActivity.fetchedData = fetchedData; Intent intent = new Intent(activity, DisplayImageActivity.class); - intent.putExtra("ImageData", fetchedData); intent.putExtra("Title", result); activity.startActivity(intent); } diff --git a/android/app/src/main/java/ipfs/gomobile/example/MainActivity.java b/android/app/src/main/java/ipfs/gomobile/example/MainActivity.java index fbe803b5..4e3534fe 100644 --- a/android/app/src/main/java/ipfs/gomobile/example/MainActivity.java +++ b/android/app/src/main/java/ipfs/gomobile/example/MainActivity.java @@ -1,29 +1,51 @@ package ipfs.gomobile.example; -import androidx.appcompat.app.AppCompatActivity; - +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; import android.graphics.Color; +import android.net.Uri; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; + +import java.nio.charset.StandardCharsets; import ipfs.gomobile.android.IPFS; public class MainActivity extends AppCompatActivity { + private static final String TAG = "MainActivity"; private IPFS ipfs; private TextView ipfsTitle; - private ProgressBar ipfsProgress; + private ProgressBar ipfsStartingProgress; private TextView ipfsResult; private TextView peerCounter; + private TextView onlineTitle; + private TextView offlineTitle; private Button xkcdButton; - private TextView xkcdStatus; - private ProgressBar xkcdProgress; - private TextView xkcdError; + private Button shareButton; + private Button fetchButton; + private TextView ipfsStatus; + private ProgressBar ipfsProgress; + private TextView ipfsError; private PeerCounter peerCounterUpdater; @@ -41,25 +63,101 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); ipfsTitle = findViewById(R.id.ipfsTitle); - ipfsProgress = findViewById(R.id.ipfsProgress); + ipfsStartingProgress = findViewById(R.id.ipfsStartingProgress); ipfsResult = findViewById(R.id.ipfsResult); peerCounter = findViewById(R.id.peerCounter); + onlineTitle = findViewById(R.id.onlineTitle); + offlineTitle = findViewById(R.id.offlineTitle); xkcdButton = findViewById(R.id.xkcdButton); - xkcdStatus = findViewById(R.id.xkcdStatus); - xkcdProgress = findViewById(R.id.xkcdProgress); - xkcdError = findViewById(R.id.xkcdError); - - new StartIPFS(this).execute(); + shareButton = findViewById(R.id.shareButton); + fetchButton = findViewById(R.id.fetchButton); + ipfsStatus = findViewById(R.id.ipfsStatus); + ipfsProgress = findViewById(R.id.ipfsProgress); + ipfsError = findViewById(R.id.ipfsError); + + if (ContextCompat.checkSelfPermission( + getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED) { + new StartIPFS(this).execute(); + } else if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION)) { + Toast.makeText(this, R.string.ble_permissions_explain, + Toast.LENGTH_LONG).show(); + } else { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 0); + } final MainActivity activity = this; + + ActivityResultLauncher selectFileResultLauncher = registerForActivityResult( + new ActivityResultContracts.OpenDocument(), + new ActivityResultCallback() { + @Override + public void onActivityResult(Uri result) { + if (result != null) { + Log.d(TAG, String.format("onActivityResult: GetContent: Uri=%s", result.toString())); + new ShareFile(activity, result).execute(); + } + } + }); + xkcdButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new FetchRandomXKCD(activity).execute(); } }); + + shareButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + selectFileResultLauncher.launch(new String[] {"image/*"}); + } + }); + + fetchButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + IntentIntegrator integrator = new IntentIntegrator(activity); + integrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE); + integrator.setPrompt("Scan a QR code"); + integrator.setOrientationLocked(false); + integrator.setBarcodeImageEnabled(true); + integrator.initiateScan(); + new IntentIntegrator(activity).initiateScan(); + } + }); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // QR Code scan result + IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data); + if(result != null) { + if(result.getContents() == null) { + Toast.makeText(this, "Cancelled", Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(this, "Scanned: " + result.getContents(), Toast.LENGTH_LONG).show(); + new FetchFile(this, result.getContents()).execute(); + } + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] strPerm, + @NonNull int [] grantResults) { + super.onRequestPermissionsResult(requestCode, strPerm, grantResults); + + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + new StartIPFS(this).execute(); + } else { + Toast.makeText(this, R.string.ble_permissions_denied, + Toast.LENGTH_LONG).show(); + } } @Override @@ -86,17 +184,21 @@ void displayPeerIDError(String error) { ipfsTitle.setText(getString(R.string.titlePeerIDErr)); ipfsResult.setText(error); - ipfsProgress.setVisibility(View.INVISIBLE); + ipfsStartingProgress.setVisibility(View.INVISIBLE); } void displayPeerIDResult(String peerID) { ipfsTitle.setText(getString(R.string.titlePeerID)); ipfsResult.setText(peerID); - ipfsProgress.setVisibility(View.INVISIBLE); + ipfsStartingProgress.setVisibility(View.INVISIBLE); updatePeerCount(0); peerCounter.setVisibility(View.VISIBLE); + onlineTitle.setVisibility(View.VISIBLE); + offlineTitle.setVisibility(View.VISIBLE); xkcdButton.setVisibility(View.VISIBLE); + shareButton.setVisibility(View.VISIBLE); + fetchButton.setVisibility(View.VISIBLE); peerCounterUpdater = new PeerCounter(this, 1000); peerCounterUpdater.start(); @@ -106,35 +208,47 @@ void updatePeerCount(int count) { peerCounter.setText(getString(R.string.titlePeerCon, count)); } - void displayFetchProgress() { - xkcdStatus.setTextColor(ipfsTitle.getCurrentTextColor()); - xkcdStatus.setText(R.string.titleFetching); - xkcdStatus.setVisibility(View.VISIBLE); - xkcdError.setVisibility(View.INVISIBLE); - xkcdProgress.setVisibility(View.VISIBLE); + void displayStatusProgress(String text) { + ipfsStatus.setTextColor(ipfsTitle.getCurrentTextColor()); + ipfsStatus.setText(text); + ipfsStatus.setVisibility(View.VISIBLE); + ipfsError.setVisibility(View.INVISIBLE); + ipfsProgress.setVisibility(View.VISIBLE); xkcdButton.setAlpha(0.5f); xkcdButton.setClickable(false); + shareButton.setAlpha(0.5f); + shareButton.setClickable(false); + fetchButton.setAlpha(0.5f); + fetchButton.setClickable(false); } - void displayFetchSuccess() { - xkcdStatus.setVisibility(View.INVISIBLE); - xkcdProgress.setVisibility(View.INVISIBLE); + void displayStatusSuccess() { + ipfsStatus.setVisibility(View.INVISIBLE); + ipfsProgress.setVisibility(View.INVISIBLE); xkcdButton.setAlpha(1); xkcdButton.setClickable(true); + shareButton.setAlpha(1); + shareButton.setClickable(true); + fetchButton.setAlpha(1); + fetchButton.setClickable(true); } - void displayFetchError(String error) { - xkcdStatus.setTextColor(Color.RED); - xkcdStatus.setText(R.string.titleFetchingErr); + void displayStatusError(String title, String error) { + ipfsStatus.setTextColor(Color.RED); + ipfsStatus.setText(title); - xkcdProgress.setVisibility(View.INVISIBLE); - xkcdError.setVisibility(View.VISIBLE); - xkcdError.setText(error); + ipfsProgress.setVisibility(View.INVISIBLE); + ipfsError.setVisibility(View.VISIBLE); + ipfsError.setText(error); xkcdButton.setAlpha(1); xkcdButton.setClickable(true); + shareButton.setAlpha(1); + shareButton.setClickable(true); + fetchButton.setAlpha(1); + fetchButton.setClickable(true); } static String exceptionToString(Exception error) { @@ -146,4 +260,15 @@ static String exceptionToString(Exception error) { return string; } + + public static String bytesToHex(byte[] bytes) { + final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); + byte[] hexChars = new byte[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars, StandardCharsets.UTF_8); + } } diff --git a/android/app/src/main/java/ipfs/gomobile/example/ShareFile.java b/android/app/src/main/java/ipfs/gomobile/example/ShareFile.java new file mode 100644 index 00000000..8dfc276c --- /dev/null +++ b/android/app/src/main/java/ipfs/gomobile/example/ShareFile.java @@ -0,0 +1,118 @@ +package ipfs.gomobile.example; + +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; + +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Random; + +import ipfs.gomobile.android.IPFS; + +final class ShareFile extends AsyncTask { + private static final String TAG = "ShareFile"; + + private final WeakReference activityRef; + private final Uri fileUri; + private ByteArrayOutputStream buffer; + private boolean backgroundError; + + + ShareFile(MainActivity activity, Uri file) { + activityRef = new WeakReference<>(activity); + this.fileUri = file; + } + + @Override + protected void onPreExecute() { + MainActivity activity = activityRef.get(); + if (activity == null || activity.isFinishing()) return; + + activity.displayStatusProgress(activity.getString(R.string.titleImageSharing)); + } + + private void readFile(Uri file) throws Exception { + MainActivity activity = activityRef.get(); + if (activity == null || activity.isFinishing()) return ; + + try { + InputStream is = activity.getContentResolver().openInputStream(file); + buffer = new ByteArrayOutputStream(); + + int nRead; + byte[] data = new byte[4096]; + + while ((nRead = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, nRead); + } + +// Log.d(TAG, "shared file data=" + MainActivity.bytesToHex(buffer.toByteArray())); + } catch (FileNotFoundException e) { + throw new Exception("File not found", e); + } catch (IOException e) { + throw new Exception("Failed to read file", e); + } + } + + @Override + protected String doInBackground(Void... v) { + MainActivity activity = activityRef.get(); + if (activity == null || activity.isFinishing()) { + cancel(true); + return null; + } + + IPFS ipfs = activity.getIpfs(); + + try { + readFile(fileUri); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream( ); + outputStream.write("--------------------------5f505897199c8c52\r\n".getBytes()); + outputStream.write("Content-Disposition: form-data; name=\"file\"\r\n".getBytes()); + outputStream.write("Content-Type: application/octet-stream\r\n\r\n".getBytes()); + outputStream.write(buffer.toByteArray()); + outputStream.write("\r\n\r\n--------------------------5f505897199c8c52--".getBytes()); + + byte body[] = outputStream.toByteArray(); + + ArrayList jsonList = ipfs.newRequest("add") + .withHeader("Content-Type", "multipart/form-data; boundary=------------------------5f505897199c8c52") + .withBody(body) + .sendToJSONList(); + + String cid = jsonList.get(0).getString("Hash"); + Log.d(TAG, "cid is " + cid); + return cid; + } catch (Exception err) { + backgroundError = true; + return MainActivity.exceptionToString(err); + } + } + + protected void onPostExecute(String result) { + MainActivity activity = activityRef.get(); + if (activity == null || activity.isFinishing()) return; + + if (backgroundError) { + activity.displayStatusError(activity.getString(R.string.titleImageSharingErr), result); + Log.e(TAG, "IPFS add error: " + result); + } else { + activity.displayStatusSuccess(); + + Intent intent = new Intent(activity, ShowQRCode.class); + intent.putExtra("cid", result); + activity.startActivity(intent); + } + } +} diff --git a/android/app/src/main/java/ipfs/gomobile/example/ShowQRCode.java b/android/app/src/main/java/ipfs/gomobile/example/ShowQRCode.java new file mode 100644 index 00000000..d91ae607 --- /dev/null +++ b/android/app/src/main/java/ipfs/gomobile/example/ShowQRCode.java @@ -0,0 +1,52 @@ +package ipfs.gomobile.example; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.Log; +import android.widget.ImageView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.journeyapps.barcodescanner.BarcodeEncoder; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class ShowQRCode extends AppCompatActivity { + private static final String TAG = "ShowQRCode"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_display_image); + + Intent intent = getIntent(); + String cid = intent.getExtras().getString("cid"); + + showQRCode(cid); + } + + private void showQRCode(String cid) { + MultiFormatWriter multiFormatWriter = new MultiFormatWriter(); + ImageView imageView = findViewById(R.id.imageView); + + try { + BitMatrix bitMatrix = multiFormatWriter.encode(cid, BarcodeFormat.QR_CODE,200,200); + BarcodeEncoder barcodeEncoder = new BarcodeEncoder(); + Bitmap bitmap = barcodeEncoder.createBitmap(bitMatrix); + imageView.setImageBitmap(bitmap); + } catch (WriterException e) { + e.printStackTrace(); + } + } +} diff --git a/android/app/src/main/java/ipfs/gomobile/example/StartIPFS.java b/android/app/src/main/java/ipfs/gomobile/example/StartIPFS.java index b7baf889..cb6c9340 100644 --- a/android/app/src/main/java/ipfs/gomobile/example/StartIPFS.java +++ b/android/app/src/main/java/ipfs/gomobile/example/StartIPFS.java @@ -13,7 +13,7 @@ final class StartIPFS extends AsyncTask { private static final String TAG = "StartIPFS"; - private WeakReference activityRef; + private final WeakReference activityRef; private boolean backgroundError; StartIPFS(MainActivity activity) { diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index bee1c358..58dcfb02 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -10,9 +10,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginStart="40dp" - android:layout_marginLeft="40dp" android:layout_marginEnd="40dp" - android:layout_marginRight="40dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -22,7 +20,7 @@ android:id="@+id/ipfsTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="160dp" + android:layout_marginTop="100dp" android:text="@string/titleStarting" android:textSize="18sp" android:textStyle="bold" @@ -31,7 +29,7 @@ app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@+id/onlineTitle" /> + app:layout_constraintTop_toBottomOf="@+id/linearLayout" /> + app:layout_constraintTop_toBottomOf="@+id/ipfsStatus" /> + app:layout_constraintTop_toBottomOf="@+id/ipfsStatus" /> + + + + + + + +