From 1ca9f45c66c9ea03090b232993760f4eb4d758bc Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Sat, 12 Mar 2022 18:54:27 +0100 Subject: [PATCH] android: return to android playstore This PR intends to return the perkeep app to the android playstore * Bump to Gradle 7.1.2, Android SKD 32 * Intents need to get the flag PendingIntent.FLAG_IMMUTABLE/MUTABLE * Assets are no longer executable, get around that by using 'extractNativeLibs=true' and executing from the lib path * Apply several refactoring suggestions by Android Studio * Rework resources Makefile * Rename camlistore to perkeep where appropriate ( not the app id ) * Remove AsyncTask; fix Typo in Upload Thread; some refactorings * Fix auto upload; fix double spurious double uploads * Retry getFileDescriptor a few times since it races with inotify * Add /Pictures to watched directories * Delegate to pk-put to get version * Fix build.gradle and Makefile to publish apk --- clients/android/.gitignore | 3 + clients/android/app/Makefile | 39 +++ clients/android/app/build.gradle | 59 ++-- .../org/camlistore/CamliActivityTest.java | 32 -- .../src/androidTest/res/values/strings.xml | 2 +- .../android/app/src/main/AndroidManifest.xml | 69 ++--- .../aidl/org/camlistore/IStatusCallback.aidl | 2 +- .../aidl/org/camlistore/IUploadService.aidl | 2 +- .../android/app/src/main/assets/.gitignore | 2 - clients/android/app/src/main/assets/Makefile | 17 -- .../android/app/src/main/assets/README.txt | 1 - .../java/org/camlistore/OnAlarmReceiver.java | 2 +- .../java/org/camlistore/OnBootReceiver.java | 14 +- ...amliActivity.java => PerkeepActivity.java} | 242 +++++++-------- ...Observer.java => PerkeepFileObserver.java} | 35 ++- .../main/java/org/camlistore/Preferences.java | 43 ++- .../java/org/camlistore/ProfilesActivity.java | 67 ++--- .../java/org/camlistore/QRPreference.java | 1 - .../main/java/org/camlistore/QueuedFile.java | 14 +- .../java/org/camlistore/SettingsActivity.java | 71 ++--- .../org/camlistore/UploadApplication.java | 94 ++---- .../java/org/camlistore/UploadService.java | 284 +++++++----------- .../java/org/camlistore/UploadThread.java | 84 +++--- .../src/main/java/org/camlistore/Util.java | 130 +------- .../app/src/main/res/xml/preferences.xml | 26 +- .../android/app/src/main/res/xml/profiles.xml | 4 +- clients/android/build.gradle | 3 +- clients/android/gradle.properties | 15 + .../gradle/wrapper/gradle-wrapper.properties | 6 +- 29 files changed, 539 insertions(+), 824 deletions(-) create mode 100644 clients/android/app/Makefile delete mode 100644 clients/android/app/src/androidTest/java/org/camlistore/CamliActivityTest.java delete mode 100644 clients/android/app/src/main/assets/.gitignore delete mode 100644 clients/android/app/src/main/assets/Makefile delete mode 100644 clients/android/app/src/main/assets/README.txt rename clients/android/app/src/main/java/org/camlistore/{CamliActivity.java => PerkeepActivity.java} (55%) rename clients/android/app/src/main/java/org/camlistore/{CamliFileObserver.java => PerkeepFileObserver.java} (63%) create mode 100644 clients/android/gradle.properties diff --git a/clients/android/.gitignore b/clients/android/.gitignore index 48dba237c..98ffeeeea 100644 --- a/clients/android/.gitignore +++ b/clients/android/.gitignore @@ -2,7 +2,10 @@ build gen bin local.properties +keystore.properties test/local.properties test/build test/gen test/bin +app/libs +app/release \ No newline at end of file diff --git a/clients/android/app/Makefile b/clients/android/app/Makefile new file mode 100644 index 000000000..4c3d4f983 --- /dev/null +++ b/clients/android/app/Makefile @@ -0,0 +1,39 @@ +REPOROOT=$(shell git rev-parse --show-toplevel) +GOBIN=$(shell go env GOPATH)/bin + +BINNAME=libpkput.so +LIBDIR=libs + +ARMLIB=$(LIBDIR)/armeabi-v7a +ARMLIB64=$(LIBDIR)/arm64-v8a +X86LIB64=$(LIBDIR)/x86_64 + +ARMPKPUT=$(ARMLIB)/$(BINNAME) +ARMPKPUT64=$(ARMLIB64)/$(BINNAME) +X86PKPUT64=$(X86LIB64)/$(BINNAME) + +all: $(ARMPKPUT) $(ARMPKPUT64) $(X86PKPUT64) + +clean: + rm -rf $(LIBDIR) + +$(ARMLIB): + mkdir -p $(ARMLIB) + +$(ARMLIB64): + mkdir -p $(ARMLIB64) + +$(X86LIB64): + mkdir -p $(X86LIB64) + +$(ARMPKPUT): $(ARMLIB) + cd $(REPOROOT) && go run make.go --os=linux --arch=arm --targets=perkeep.org/cmd/pk-put + cp $(GOBIN)/linux_arm/pk-put $(ARMPKPUT) + +$(ARMPKPUT64): $(ARMLIB64) + cd $(REPOROOT) && go run make.go --os=linux --arch=arm64 --targets=perkeep.org/cmd/pk-put + cp $(GOBIN)/linux_arm64/pk-put $(ARMPKPUT64) + +$(X86PKPUT64): $(X86LIB64) + cd $(REPOROOT) && go run make.go --os=linux --arch=amd64 --targets=perkeep.org/cmd/pk-put + cp $(GOBIN)/pk-put $(X86PKPUT64) diff --git a/clients/android/app/build.gradle b/clients/android/app/build.gradle index 4075334af..21d785c15 100644 --- a/clients/android/app/build.gradle +++ b/clients/android/app/build.gradle @@ -5,50 +5,51 @@ */ apply plugin: 'com.android.application' -// Create a variable called keystorePropertiesFile, and initialize it to your -// keystore.properties file, in the rootProject folder. def keystorePropertiesFile = rootProject.file("keystore.properties") - -// Initialize a new Properties() object called keystoreProperties. def keystoreProperties = new Properties() - -// Load your keystore.properties file into the keystoreProperties object. keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { - compileSdkVersion 27 - - // TODO(mpl): this should make signing the apk automatic when building the - // release flavor, but it does not seem to. figure out why. use Makefile in the - // meantime. - signingConfigs { - config { - keyAlias keystoreProperties['keyAlias'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] - } - } + compileSdkVersion 32 defaultConfig { applicationId "org.camlistore" - minSdkVersion 14 - // Stay below API 26 for a while, because it deprecates the Notification Builder - // constructor we're using. - targetSdkVersion 26 - // integer. used by android to prevent downgrades. not seen by user. - versionCode 4 - // version shown to the user in play store. - versionName "0.10" + minSdkVersion 26 + targetSdkVersion 32 + versionCode 7 + versionName "0.11" } + + packagingOptions { + jniLibs.useLegacyPackaging = true + } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } + + signingConfigs { + release { + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + + v1SigningEnabled true + v2SigningEnabled true + } + } + buildTypes { release { minifyEnabled false + signingConfig signingConfigs.release } } } dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.android.support:appcompat-v7:26.0.0' - implementation 'com.android.support:support-compat:26.0.0' + implementation 'androidx.appcompat:appcompat:1.4.1' } diff --git a/clients/android/app/src/androidTest/java/org/camlistore/CamliActivityTest.java b/clients/android/app/src/androidTest/java/org/camlistore/CamliActivityTest.java deleted file mode 100644 index a8fb6633b..000000000 --- a/clients/android/app/src/androidTest/java/org/camlistore/CamliActivityTest.java +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2011 The Perkeep 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 - - 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 org.camlistore; - -import android.test.ActivityInstrumentationTestCase2; - -public class CamliActivityTest extends ActivityInstrumentationTestCase2 { - - public CamliActivityTest(String pkg, Class activityClass) { - super(pkg, activityClass); - // TODO Auto-generated constructor stub - } - - public void testSanity() { - assertEquals(2, 1 + 1); - assertEquals(4, 2 + 2); - } -} diff --git a/clients/android/app/src/androidTest/res/values/strings.xml b/clients/android/app/src/androidTest/res/values/strings.xml index 45c922e38..400d1e11e 100644 --- a/clients/android/app/src/androidTest/res/values/strings.xml +++ b/clients/android/app/src/androidTest/res/values/strings.xml @@ -1,5 +1,5 @@ Hello World! - camlistoreTest + perkeepTest diff --git a/clients/android/app/src/main/AndroidManifest.xml b/clients/android/app/src/main/AndroidManifest.xml index 11f562d39..32c577dc8 100644 --- a/clients/android/app/src/main/AndroidManifest.xml +++ b/clients/android/app/src/main/AndroidManifest.xml @@ -2,31 +2,32 @@ - - + + - + - + - - + + @@ -43,37 +44,33 @@ - - + - - + - - - - - - - - + + + + + - - - - - + + + + - - + + - - + + - - + + diff --git a/clients/android/app/src/main/aidl/org/camlistore/IStatusCallback.aidl b/clients/android/app/src/main/aidl/org/camlistore/IStatusCallback.aidl index 0849c05af..39a6d4016 100644 --- a/clients/android/app/src/main/aidl/org/camlistore/IStatusCallback.aidl +++ b/clients/android/app/src/main/aidl/org/camlistore/IStatusCallback.aidl @@ -22,7 +22,7 @@ oneway interface IStatusCallback { void setUploadStatsText(String text); // big box void setUploadErrorsText(String text); void setUploading(boolean uploading); - + // done: acknowledged by server // inFlight: those written to the server, but no reply yet (i.e. large HTTP POST body) (does NOT include the "done" ones) // total: "this batch" size. reset on transition from 0 -> 1 blobs remain. diff --git a/clients/android/app/src/main/aidl/org/camlistore/IUploadService.aidl b/clients/android/app/src/main/aidl/org/camlistore/IUploadService.aidl index b96f0ed85..f027a5511 100644 --- a/clients/android/app/src/main/aidl/org/camlistore/IUploadService.aidl +++ b/clients/android/app/src/main/aidl/org/camlistore/IUploadService.aidl @@ -43,7 +43,7 @@ interface IUploadService { // Stop stop uploads, clear queues. void stopEverything(); - + // For the SettingsActivity void setBackgroundWatchersEnabled(boolean enabled); diff --git a/clients/android/app/src/main/assets/.gitignore b/clients/android/app/src/main/assets/.gitignore deleted file mode 100644 index 8aea5594a..000000000 --- a/clients/android/app/src/main/assets/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -pk-put.arm -pk-put-version.txt diff --git a/clients/android/app/src/main/assets/Makefile b/clients/android/app/src/main/assets/Makefile deleted file mode 100644 index 994a02e89..000000000 --- a/clients/android/app/src/main/assets/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -# TODO(mpl): update this file. -# To use this Makefile, first: -# -# $ cd $GOROOT/src -# $ GOOS=linux GOARCH=arm ./make.bash -# -# TODO: have make.go bootstrap that above when necessary, running "go env" to find the GOROOT and -# mirror it all into a separate writable GOROOT under $CAMROOT/tmp and bootstrap -# it with "GOOS=linux GOARCH=arm make.bash". -all: - (cd ../../.. && go run make.go --os=linux --arch=arm --targets=camlistore.org/cmd/pk-put) - cp -p ../../../bin/linux_arm/pk-put pk-put.arm - ../../../misc/gitversion > pk-put-version.txt - mkdir -p ../gen/org/camlistore - /bin/echo -n "package org.camlistore; public final class ChildProcessConfig { // " > ../gen/org/camlistore/ChildProcessConfig.java - openssl sha1 pk-put.arm >> ../gen/org/camlistore/ChildProcessConfig.java - /bin/echo "}" >> ../gen/org/camlistore/ChildProcessConfig.java diff --git a/clients/android/app/src/main/assets/README.txt b/clients/android/app/src/main/assets/README.txt deleted file mode 100644 index 06b66ec23..000000000 --- a/clients/android/app/src/main/assets/README.txt +++ /dev/null @@ -1 +0,0 @@ -Put pk-put.arm here. It's in .gitignore because it's 6.5 MB. diff --git a/clients/android/app/src/main/java/org/camlistore/OnAlarmReceiver.java b/clients/android/app/src/main/java/org/camlistore/OnAlarmReceiver.java index 95e6b2171..1d2fb63f6 100644 --- a/clients/android/app/src/main/java/org/camlistore/OnAlarmReceiver.java +++ b/clients/android/app/src/main/java/org/camlistore/OnAlarmReceiver.java @@ -22,7 +22,7 @@ import android.content.Intent; import android.util.Log; public class OnAlarmReceiver extends BroadcastReceiver { - private static final String TAG = "Camli_OnAlarmReceiver"; + private static final String TAG = "perkeep_OnAlarmReceiver"; @Override public void onReceive(Context context, Intent intent) { diff --git a/clients/android/app/src/main/java/org/camlistore/OnBootReceiver.java b/clients/android/app/src/main/java/org/camlistore/OnBootReceiver.java index e53bce288..9063aa836 100644 --- a/clients/android/app/src/main/java/org/camlistore/OnBootReceiver.java +++ b/clients/android/app/src/main/java/org/camlistore/OnBootReceiver.java @@ -25,17 +25,21 @@ import android.os.SystemClock; import android.util.Log; public class OnBootReceiver extends BroadcastReceiver { - private static final String TAG = "Camli_OnBootReceiver"; + private static final String TAG = "perkeep_OnBootReceiver"; @Override public void onReceive(Context context, Intent intent) { Log.v(TAG, "onReceive on boot"); AlarmManager alarmer = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(context, - OnAlarmReceiver.class), 0); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + context, + 0, + new Intent(context, OnAlarmReceiver.class), PendingIntent.FLAG_IMMUTABLE); - alarmer.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, - SystemClock.elapsedRealtime() + 60000, AlarmManager.INTERVAL_HALF_HOUR, + alarmer.setInexactRepeating( + AlarmManager.ELAPSED_REALTIME, + SystemClock.elapsedRealtime() + 60000, + AlarmManager.INTERVAL_HALF_HOUR, pendingIntent); } diff --git a/clients/android/app/src/main/java/org/camlistore/CamliActivity.java b/clients/android/app/src/main/java/org/camlistore/PerkeepActivity.java similarity index 55% rename from clients/android/app/src/main/java/org/camlistore/CamliActivity.java rename to clients/android/app/src/main/java/org/camlistore/PerkeepActivity.java index 930e23d49..a55f37dfd 100644 --- a/clients/android/app/src/main/java/org/camlistore/CamliActivity.java +++ b/clients/android/app/src/main/java/org/camlistore/PerkeepActivity.java @@ -31,20 +31,19 @@ import android.os.IBinder; import android.os.Looper; import android.os.MessageQueue; import android.os.RemoteException; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; import android.util.Log; import android.view.Menu; import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; -public class CamliActivity extends Activity { - private static final String TAG = "CamliActivity"; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +public class PerkeepActivity extends Activity { + private static final String TAG = "PerkeepActivity"; private static final int MENU_SETTINGS = 1; private static final int MENU_STOP = 2; @@ -66,17 +65,14 @@ public class CamliActivity extends Activity { private final Handler mHandler = new Handler(); - private final MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() { - @Override - public boolean queueIdle() { - if (mStatusTextCurrent != mStatusTextWant) { - TextView textStats = (TextView) findViewById(R.id.textStats); - mLastStatusUpdate = System.currentTimeMillis(); - mStatusTextCurrent = mStatusTextWant; - textStats.setText(mStatusTextWant); - } - return true; + private final MessageQueue.IdleHandler mIdleHandler = () -> { + if (mStatusTextCurrent != mStatusTextWant) { + TextView textStats = findViewById(R.id.textStats); + mLastStatusUpdate = System.currentTimeMillis(); + mStatusTextCurrent = mStatusTextWant; + textStats.setText(mStatusTextWant); } + return true; }; private final ServiceConnection mServiceConnection = new ServiceConnection() { @@ -97,7 +93,7 @@ public class CamliActivity extends Activity { public void onServiceDisconnected(ComponentName name) { Log.d(TAG, "Service disconnected"); mServiceStub = null; - }; + } }; @Override @@ -106,158 +102,131 @@ public class CamliActivity extends Activity { setContentView(R.layout.main); Looper.myQueue().addIdleHandler(mIdleHandler); - final Button buttonToggle = (Button) findViewById(R.id.buttonToggle); + final Button buttonToggle = findViewById(R.id.buttonToggle); - final TextView textStatus = (TextView) findViewById(R.id.textStatus); - final TextView textStats = (TextView) findViewById(R.id.textStats); - final TextView textErrors = (TextView) findViewById(R.id.textErrors); - final TextView textBlobsRemain = (TextView) findViewById(R.id.textBlobsRemain); - final TextView textUploadStatus = (TextView) findViewById(R.id.textUploadStatus); - final TextView textByteStatus = (TextView) findViewById(R.id.textByteStatus); - final ProgressBar progressBytes = (ProgressBar) findViewById(R.id.progressByteStatus); - final TextView textFileStatus = (TextView) findViewById(R.id.textFileStatus); - final ProgressBar progressFile = (ProgressBar) findViewById(R.id.progressFileStatus); + final TextView textStatus = findViewById(R.id.textStatus); + final TextView textStats = findViewById(R.id.textStats); + final TextView textErrors = findViewById(R.id.textErrors); + final TextView textBlobsRemain = findViewById(R.id.textBlobsRemain); + final TextView textUploadStatus = findViewById(R.id.textUploadStatus); + final TextView textByteStatus = findViewById(R.id.textByteStatus); + final ProgressBar progressBytes = findViewById(R.id.progressByteStatus); + final TextView textFileStatus = findViewById(R.id.textFileStatus); + final ProgressBar progressFile = findViewById(R.id.progressFileStatus); - buttonToggle.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View btn) { - Log.d(TAG, "button click! text=" + buttonToggle.getText()); - if (getString(R.string.pause).equals(buttonToggle.getText())) { - try { - Log.d(TAG, "Pausing.."); - mServiceStub.pause(); - } catch (RemoteException e) { - } - } else if (getString(R.string.resume).equals(buttonToggle.getText())) { - try { - Log.d(TAG, "Resuming.."); - mServiceStub.resume(); - } catch (RemoteException e) { - } + buttonToggle.setOnClickListener(btn -> { + Log.d(TAG, "button click! text=" + buttonToggle.getText()); + if (getString(R.string.pause).contentEquals(buttonToggle.getText())) { + try { + Log.d(TAG, "Pausing.."); + mServiceStub.pause(); + } catch (RemoteException ignored) { + } + } else if (getString(R.string.resume).contentEquals(buttonToggle.getText())) { + try { + Log.d(TAG, "Resuming.."); + mServiceStub.resume(); + } catch (RemoteException ignored) { } } }); mCallback = new IStatusCallback.Stub() { - private volatile int mLastBlobsUploadRemain = 0; - private volatile int mLastBlobsDigestRemain = 0; + private final int mLastBlobsUploadRemain = 0; + private final int mLastBlobsDigestRemain = 0; @Override - public void logToClient(String stuff) throws RemoteException { + public void logToClient(String stuff) { // TODO Auto-generated method stub } @Override - public void setUploading(final boolean uploading) throws RemoteException { - mHandler.post(new Runnable() { - @Override - public void run() { - if (uploading) { - buttonToggle.setText(R.string.pause); - textStatus.setText(R.string.uploading); - textErrors.setText(""); - } else if (mLastBlobsDigestRemain > 0) { - buttonToggle.setText(R.string.pause); - textStatus.setText(R.string.digesting); - } else { - buttonToggle.setText(R.string.resume); - int stepsRemain = mLastBlobsUploadRemain + mLastBlobsDigestRemain; - textStatus.setText(stepsRemain > 0 ? "Paused." : "Idle."); - } + public void setUploading(final boolean uploading) { + mHandler.post(() -> { + if (uploading) { + buttonToggle.setText(R.string.pause); + textStatus.setText(R.string.uploading); + textErrors.setText(""); + } else if (mLastBlobsDigestRemain > 0) { + buttonToggle.setText(R.string.pause); + textStatus.setText(R.string.digesting); + } else { + buttonToggle.setText(R.string.resume); + int stepsRemain = mLastBlobsUploadRemain + mLastBlobsDigestRemain; + textStatus.setText(stepsRemain > 0 ? "Paused." : "Idle."); } }); } @Override - public void setFileStatus(final int done, final int inFlight, final int total) throws RemoteException { - mHandler.post(new Runnable() { - @Override - public void run() { - boolean finished = (done == total && mLastBlobsDigestRemain == 0); - buttonToggle.setEnabled(!finished); - progressFile.setMax(total); - progressFile.setProgress(done); - progressFile.setSecondaryProgress(done + inFlight); - if (finished) { - buttonToggle.setText(getString(R.string.pause_resume)); - } - - StringBuilder filesUploaded = new StringBuilder(40); - if (done < 2) { - filesUploaded.append(done).append(" file uploaded"); - } else { - filesUploaded.append(done).append(" files uploaded"); - } - textFileStatus.setText(filesUploaded.toString()); - - StringBuilder sb = new StringBuilder(40); - sb.append("Files to upload: ").append(total - done); - textBlobsRemain.setText(sb.toString()); + public void setFileStatus(final int done, final int inFlight, final int total) { + mHandler.post(() -> { + boolean finished = (done == total && mLastBlobsDigestRemain == 0); + buttonToggle.setEnabled(!finished); + progressFile.setMax(total); + progressFile.setProgress(done); + progressFile.setSecondaryProgress(done + inFlight); + if (finished) { + buttonToggle.setText(getString(R.string.pause_resume)); } + + StringBuilder filesUploaded = new StringBuilder(40); + if (done < 2) { + filesUploaded.append(done).append(" file uploaded"); + } else { + filesUploaded.append(done).append(" files uploaded"); + } + textFileStatus.setText(filesUploaded.toString()); + + textBlobsRemain.setText("Files to upload: " + (total - done)); }); } @Override - public void setByteStatus(final long done, final int inFlight, final long total) throws RemoteException { - mHandler.post(new Runnable() { - @Override - public void run() { - // setMax takes an (signed) int, but 2GB is a totally - // reasonable upload size, so use units of 1KB instead. - progressBytes.setMax((int) (total / 1024L)); - progressBytes.setProgress((int) (done / 1024L)); - // TODO: renable once pk-put properly sends inflight information - // progressBytes.setSecondaryProgress(progressBytes.getProgress() + inFlight / 1024); + public void setByteStatus(final long done, final int inFlight, final long total) { + mHandler.post(() -> { + // setMax takes an (signed) int, but 2GB is a totally + // reasonable upload size, so use units of 1KB instead. + progressBytes.setMax((int) (total / 1024L)); + progressBytes.setProgress((int) (done / 1024L)); + // TODO: renable once pk-put properly sends inflight information + // progressBytes.setSecondaryProgress(progressBytes.getProgress() + inFlight / 1024); - StringBuilder bytesUploaded = new StringBuilder(40); - if (done < 2) { - bytesUploaded.append(done).append(" byte uploaded"); - } else { - bytesUploaded.append(done).append(" bytes uploaded"); - } - textByteStatus.setText(bytesUploaded.toString()); + StringBuilder bytesUploaded = new StringBuilder(40); + if (done < 2) { + bytesUploaded.append(done).append(" byte uploaded"); + } else { + bytesUploaded.append(done).append(" bytes uploaded"); } + textByteStatus.setText(bytesUploaded.toString()); }); } @Override - public void setUploadStatusText(final String text) throws RemoteException { - mHandler.post(new Runnable() { - @Override - public void run() { - textUploadStatus.setText(text); - } - }); + public void setUploadStatusText(final String text) { + mHandler.post(() -> textUploadStatus.setText(text)); } @Override - public void setUploadStatsText(final String text) throws RemoteException { + public void setUploadStatsText(final String text) { // We were getting these status updates so quickly that the calls to TextView.setText // were consuming all CPU on the main thread and it was stalling the main thread // for seconds, sometimes even triggering device freezes. Ridiculous. So instead, // only update this every 30 milliseconds, otherwise wait for the looper to be idle // to update it. - mHandler.post(new Runnable() { - @Override - public void run() { - mStatusTextWant = text; - long now = System.currentTimeMillis(); - if (mLastStatusUpdate < now - 30) { - mStatusTextCurrent = mStatusTextWant; - textStats.setText(mStatusTextWant); - mLastStatusUpdate = System.currentTimeMillis(); - } + mHandler.post(() -> { + mStatusTextWant = text; + long now = System.currentTimeMillis(); + if (mLastStatusUpdate < now - 30) { + mStatusTextCurrent = mStatusTextWant; + textStats.setText(mStatusTextWant); + mLastStatusUpdate = System.currentTimeMillis(); } }); } - public void setUploadErrorsText(final String text) throws RemoteException { - mHandler.post(new Runnable() { - @Override - public void run() { - textErrors.setText(text); - } - }); + public void setUploadErrorsText(final String text) { + mHandler.post(() -> textErrors.setText(text)); } }; @@ -321,7 +290,7 @@ public class CamliActivity extends Activity { ProfilesActivity.show(this); break; case MENU_VERSION: - Toast.makeText(this, "pk-put version: " + ((UploadApplication) getApplication()).getCamputVersion(), Toast.LENGTH_LONG).show(); + Toast.makeText(this, "pk-put version: " + ((UploadApplication) getApplication()).getPkPutVersion(), Toast.LENGTH_LONG).show(); break; case MENU_UPLOAD_ALL: Intent uploadAll = new Intent(UploadService.INTENT_UPLOAD_ALL); @@ -353,6 +322,8 @@ public class CamliActivity extends Activity { super.onResume(); // Check for the right to read the user's files. + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, @@ -388,18 +359,15 @@ public class CamliActivity extends Activity { Log.d(TAG, "onResume; action=" + action); if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { - setIntent(new Intent(this, CamliActivity.class)); + setIntent(new Intent(this, PerkeepActivity.class)); } else { - Log.d(TAG, "Normal CamliActivity viewing."); + Log.d(TAG, "Normal perkeepActivity viewing."); } } @Override - public void onRequestPermissionsResult(int requestCode, - String permissions[], int[] grantResults) { - switch (requestCode) { - case READ_EXTERNAL_STORAGE_PERMISSION_RESPONSE: { - // If request is cancelled, the result arrays are empty. + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == READ_EXTERNAL_STORAGE_PERMISSION_RESPONSE) {// If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "User authorized us to read his files."); } else { @@ -407,8 +375,6 @@ public class CamliActivity extends Activity { Log.d(TAG, "Permission to read files denied by user."); System.exit(1); } - return; - } } } } diff --git a/clients/android/app/src/main/java/org/camlistore/CamliFileObserver.java b/clients/android/app/src/main/java/org/camlistore/PerkeepFileObserver.java similarity index 63% rename from clients/android/app/src/main/java/org/camlistore/CamliFileObserver.java rename to clients/android/app/src/main/java/org/camlistore/PerkeepFileObserver.java index da6775212..13a6aaa59 100644 --- a/clients/android/app/src/main/java/org/camlistore/CamliFileObserver.java +++ b/clients/android/app/src/main/java/org/camlistore/PerkeepFileObserver.java @@ -17,6 +17,7 @@ limitations under the License. package org.camlistore; import java.io.File; +import java.nio.file.Paths; import android.net.Uri; import android.os.FileObserver; @@ -25,13 +26,13 @@ import android.util.Log; import org.camlistore.IUploadService.Stub; -public class CamliFileObserver extends FileObserver { - private static final String TAG = "CamliFileObserver"; +public class PerkeepFileObserver extends FileObserver { + private static final String TAG = "PerkeepFileObserver"; private final File mDirectory; private final Stub mServiceStub; - public CamliFileObserver(IUploadService.Stub service, File directory) { + public PerkeepFileObserver(IUploadService.Stub service, File directory) { super(directory.getAbsolutePath(), FileObserver.CLOSE_WRITE | FileObserver.MOVED_TO); // TODO: Docs say: "The monitored file or directory must exist at this // time, or else no events will be reported (even if it appears @@ -45,21 +46,29 @@ public class CamliFileObserver extends FileObserver { @Override public void onEvent(int event, String path) { - if (path == null) { - // It's null for certain directory-level events. + if (!shouldActOnEvent(path)){ return; } - - // Note from docs: - // "This method is invoked on a special FileObserver thread." - - // Order in which we get events for a new camera picture: - // CREATE, OPEN, MODIFY, [OPEN, CLOSE_NOWRITE], CLOSE_WRITE File fullFile = new File(mDirectory, path); Log.d(TAG, "event " + event + " for " + fullFile.getAbsolutePath()); try { - mServiceStub.enqueueUpload(Uri.fromFile(fullFile)); - } catch (RemoteException e) { + mServiceStub.enqueueUpload(Uri.fromFile(fullFile)); + } catch (RemoteException ignored) { } } + + private boolean shouldActOnEvent(String path) { + // It's null for certain directory-level events. + if (path == null) { + return false; + } + // Taking a photo will generate a ".pending-*" file before moving it into the proper + // path leading to double uploads sometimes ( race between enqueue and upload). We + // get around that by the heuristic of ignoring ".pending" filenames here. + if (Paths.get(path).getFileName().toString().startsWith(".pending")) { + return false; + } + // act on all other events + return true; + } } diff --git a/clients/android/app/src/main/java/org/camlistore/Preferences.java b/clients/android/app/src/main/java/org/camlistore/Preferences.java index e3dec7adb..1809a7302 100644 --- a/clients/android/app/src/main/java/org/camlistore/Preferences.java +++ b/clients/android/app/src/main/java/org/camlistore/Preferences.java @@ -20,31 +20,30 @@ import android.content.Context; import android.content.SharedPreferences; public final class Preferences { - private static final String TAG = "Preferences"; - public static final String NAME = "CamliUploader"; + public static final String NAME = "perkeepUploader"; // key/value store file where we keep the profile names - public static final String PROFILES_FILE = "CamliUploader_profiles"; + public static final String PROFILES_FILE = "perkeepUploader_profiles"; // key to the set of profile names - public static final String PROFILES = "camli.profiles"; + public static final String PROFILES = "perkeep.profiles"; // key to the currently selected profile - public static final String PROFILE = "camli.profile"; + public static final String PROFILE = "perkeep.profile"; // for the preference element that lets us create a new profile name - public static final String NEWPROFILE = "camli.newprofile"; + public static final String NEWPROFILE = "perkeep.newprofile"; - public static final String HOST = "camli.host"; + public static final String HOST = "perkeep.host"; // TODO(mpl): list instead of single string later? seems overkill for now. - public static final String USERNAME = "camli.username"; - public static final String PASSWORD = "camli.password"; - public static final String AUTO = "camli.auto"; - public static final String AUTO_OPTS = "camli.auto.opts"; - public static final String MAX_CACHE_MB = "camli.max_cache_mb"; - public static final String DEV_IP = "camli.dev_ip"; - public static final String AUTO_REQUIRE_POWER = "camli.auto.require_power"; - public static final String AUTO_REQUIRE_WIFI = "camli.auto.require_wifi"; - public static final String AUTO_REQUIRED_WIFI_SSID = "camli.auto.required_wifi_ssid"; - public static final String AUTO_DIR_PHOTOS = "camli.auto.photos"; - public static final String AUTO_DIR_MYTRACKS = "camli.auto.mytracks"; + public static final String USERNAME = "perkeep.username"; + public static final String PASSWORD = "perkeep.password"; + public static final String AUTO = "perkeep.auto"; + public static final String AUTO_OPTS = "perkeep.auto.opts"; + public static final String MAX_CACHE_MB = "perkeep.max_cache_mb"; + public static final String DEV_IP = "perkeep.dev_ip"; + public static final String AUTO_REQUIRE_POWER = "perkeep.auto.require_power"; + public static final String AUTO_REQUIRE_WIFI = "perkeep.auto.require_wifi"; + public static final String AUTO_REQUIRED_WIFI_SSID = "perkeep.auto.required_wifi_ssid"; + public static final String AUTO_DIR_PHOTOS = "perkeep.auto.photos"; + public static final String AUTO_DIR_MYTRACKS = "perkeep.auto.mytracks"; private final SharedPreferences mSP; @@ -57,7 +56,7 @@ public final class Preferences { SharedPreferences profiles = ctx.getSharedPreferences(PROFILES_FILE, 0); String currentProfile = profiles.getString(Preferences.PROFILE, "default"); if (currentProfile.equals("default")) { - // Special case: we keep CamliUploader as the conf file name by default, to stay + // Special case: we keep perkeepUploader as the conf file name by default, to stay // backwards compatible. return NAME; } @@ -84,10 +83,6 @@ public final class Preferences { return Integer.parseInt(mSP.getString(MAX_CACHE_MB, "256")); } - public long maxCacheBytes() { - return maxCacheMb() * 1024 * 1024; - } - public boolean autoDirPhotos() { return mSP.getBoolean(AUTO_DIR_PHOTOS, true); } @@ -106,7 +101,7 @@ public final class Preferences { public String username() { if (inDevMode()) { - return "camlistore"; + return "perkeep"; } return mSP.getString(USERNAME, ""); } diff --git a/clients/android/app/src/main/java/org/camlistore/ProfilesActivity.java b/clients/android/app/src/main/java/org/camlistore/ProfilesActivity.java index 611a07353..9dd161cd5 100644 --- a/clients/android/app/src/main/java/org/camlistore/ProfilesActivity.java +++ b/clients/android/app/src/main/java/org/camlistore/ProfilesActivity.java @@ -30,7 +30,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.preference.ListPreference; import android.preference.EditTextPreference; -import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceActivity; import android.util.Log; @@ -68,41 +67,34 @@ public class ProfilesActivity extends PreferenceActivity { refreshProfileRef(); mNewProfilePref = (EditTextPreference) findPreference(Preferences.NEWPROFILE); - OnPreferenceChangeListener onChange = new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference pref, Object newValue) { - // Note: newValue isn't yet persisted, but easiest to update the - // UI here. - if (!(newValue instanceof String)) { - return false; - } - String newStr = (String) newValue; - if (pref == mProfilePref) { - updateProfilesSummary(newStr); - } else if (pref == mNewProfilePref) { - updateProfilesList(newStr); - return false; // do not actually persist it. - } - // TODO(mpl): some way to remove a profile. - return true; // yes, persist it + OnPreferenceChangeListener onChange = (pref, newValue) -> { + // Note: newValue isn't yet persisted, but easiest to update the + // UI here. + if (!(newValue instanceof String)) { + return false; } + String newStr = (String) newValue; + if (pref == mProfilePref) { + updateProfilesSummary(newStr); + } else if (pref == mNewProfilePref) { + updateProfilesList(newStr); + return false; // do not actually persist it. + } + // TODO(mpl): some way to remove a profile. + return true; // yes, persist it }; mProfilePref.setOnPreferenceChangeListener(onChange); mNewProfilePref.setOnPreferenceChangeListener(onChange); } - private final SharedPreferences.OnSharedPreferenceChangeListener prefChangedHandler = new SharedPreferences.OnSharedPreferenceChangeListener() { - @Override - public void onSharedPreferenceChanged(SharedPreferences sp, String key) { - if (mServiceStub != null) { - try { - mServiceStub.reloadSettings(); - } catch (RemoteException e) { - // Ignore. - } + private final SharedPreferences.OnSharedPreferenceChangeListener prefChangedHandler = (sp, key) -> { + if (mServiceStub != null) { + try { + mServiceStub.reloadSettings(); + } catch (RemoteException ignored) { } - } + }; @Override @@ -111,17 +103,18 @@ public class ProfilesActivity extends PreferenceActivity { refreshProfileRef(); updatePreferenceSummaries(); mSharedPrefs.registerOnSharedPreferenceChangeListener(prefChangedHandler); - bindService(new Intent(this, UploadService.class), mServiceConnection, - Context.BIND_AUTO_CREATE); + bindService( + new Intent(this, UploadService.class), + mServiceConnection, + Context.BIND_AUTO_CREATE + ); } @Override protected void onPause() { super.onPause(); mSharedPrefs.unregisterOnSharedPreferenceChangeListener(prefChangedHandler); - if (mServiceConnection != null) { - unbindService(mServiceConnection); - } + unbindService(mServiceConnection); } private void updatePreferenceSummaries() { @@ -147,11 +140,11 @@ public class ProfilesActivity extends PreferenceActivity { return; } - Set profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet()); + Set profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet<>()); profiles.add(value); Editor ed = mSharedPrefs.edit(); ed.putStringSet(Preferences.PROFILES, profiles); - ed.commit(); + ed.apply(); refreshProfileRef(); mProfilePref.setValue(value); mProfilePref.setSummary(value); @@ -161,13 +154,13 @@ public class ProfilesActivity extends PreferenceActivity { // refreshProfileRef refreshes the profiles preference list with the profile // values stored in the key/value file. private void refreshProfileRef() { - Set profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet()); + Set profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet<>()); if (profiles.isEmpty()) { // make sure there's always at least the "default" profile. profiles.add("default"); Editor ed = mSharedPrefs.edit(); ed.putStringSet(Preferences.PROFILES, profiles); - ed.commit(); + ed.apply(); } CharSequence[] listValues = profiles.toArray(new String[]{}); mProfilePref.setEntries(listValues); diff --git a/clients/android/app/src/main/java/org/camlistore/QRPreference.java b/clients/android/app/src/main/java/org/camlistore/QRPreference.java index 1842dd244..fc33a5ccd 100644 --- a/clients/android/app/src/main/java/org/camlistore/QRPreference.java +++ b/clients/android/app/src/main/java/org/camlistore/QRPreference.java @@ -1,6 +1,5 @@ package org.camlistore; -import android.app.AlertDialog; import android.content.Context; import android.preference.Preference; import android.util.AttributeSet; diff --git a/clients/android/app/src/main/java/org/camlistore/QueuedFile.java b/clients/android/app/src/main/java/org/camlistore/QueuedFile.java index f74524afc..d377832e6 100644 --- a/clients/android/app/src/main/java/org/camlistore/QueuedFile.java +++ b/clients/android/app/src/main/java/org/camlistore/QueuedFile.java @@ -18,6 +18,8 @@ package org.camlistore; import android.net.Uri; +import androidx.annotation.NonNull; + /** * Immutable struct for tuple (sha1 blobRef, URI to upload, size of blob). */ @@ -36,10 +38,6 @@ public class QueuedFile { mDiskPath = diskPath; } - public Uri getUri() { - return mUri; - } - public long getSize() { return mSize; } @@ -49,6 +47,7 @@ public class QueuedFile { return mDiskPath; } + @NonNull @Override public String toString() { return "QueuedFile [mSize=" + mSize + ", mUri=" + mUri + "]"; @@ -59,7 +58,7 @@ public class QueuedFile { final int prime = 31; int result = 1; result = prime * result + (int) (mSize ^ (mSize >>> 32)); - result = prime * result + ((mUri == null) ? 0 : mUri.hashCode()); + result = prime * result + mUri.hashCode(); return result; } @@ -74,10 +73,7 @@ public class QueuedFile { QueuedFile other = (QueuedFile) obj; if (mSize != other.mSize) return false; - if (mUri == null) { - if (other.mUri != null) - return false; - } else if (!mUri.equals(other.mUri)) + if (!mUri.equals(other.mUri)) return false; return true; } diff --git a/clients/android/app/src/main/java/org/camlistore/SettingsActivity.java b/clients/android/app/src/main/java/org/camlistore/SettingsActivity.java index 7a869be1f..34cc3c1e6 100644 --- a/clients/android/app/src/main/java/org/camlistore/SettingsActivity.java +++ b/clients/android/app/src/main/java/org/camlistore/SettingsActivity.java @@ -35,7 +35,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; -import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; @@ -106,30 +105,27 @@ public class SettingsActivity extends PreferenceActivity { maxCacheSizePref.setSummary(getString( R.string.settings_max_cache_size_summary, mPrefs.maxCacheMb())); - OnPreferenceChangeListener onChange = new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference pref, Object newValue) { - final String key = pref.getKey(); - Log.v(TAG, "preference change for: " + key); + OnPreferenceChangeListener onChange = (pref, newValue) -> { + final String key = pref.getKey(); + Log.v(TAG, "preference change for: " + key); - // Note: newValue isn't yet persisted, but easiest to update the - // UI here. - String newStr = (newValue instanceof String) ? (String) newValue - : null; - if (pref == hostPref) { - updateHostSummary(newStr); - } else if (pref == passwordPref) { - updatePasswordSummary(newStr); - } else if (pref == usernamePref) { - updateUsernameSummary(newStr); - } else if (pref == maxCacheSizePref) { - if (!updateMaxCacheSizeSummary(newStr)) - return false; - } else if (pref == devIPPref) { - updateDevIP(newStr); - } - return true; // yes, persist it + // Note: newValue isn't yet persisted, but easiest to update the + // UI here. + String newStr = (newValue instanceof String) ? (String) newValue + : null; + if (pref == hostPref) { + updateHostSummary(newStr); + } else if (pref == passwordPref) { + updatePasswordSummary(newStr); + } else if (pref == usernamePref) { + updateUsernameSummary(newStr); + } else if (pref == maxCacheSizePref) { + if (!updateMaxCacheSizeSummary(newStr)) + return false; + } else if (pref == devIPPref) { + updateDevIP(newStr); } + return true; // yes, persist it }; hostPref.setOnPreferenceChangeListener(onChange); passwordPref.setOnPreferenceChangeListener(onChange); @@ -140,7 +136,7 @@ public class SettingsActivity extends PreferenceActivity { /** * Receives the results from the custome QRPreference's call to the barcode scanner intent. - * + * * This is never called if the user doesn't have a zxing barcode scanner app installed. */ @Override @@ -161,14 +157,14 @@ public class SettingsActivity extends PreferenceActivity { * confirmNewSettingsDialog will set preferences based on the parameters * in uri. * - * It is expected the schema of uri is 'camli' and the host is 'settings'. + * It is expected the schema of uri is 'perkeep' and the host is 'settings'. * Uri parameters expected are server, certFingerprint, username, * autoUpload, maxCacheSize, and password */ - private final void confirmNewSettingsDialog(final Uri uri) { + private void confirmNewSettingsDialog(final Uri uri) { Log.v(TAG, "QR resolved to: " + uri); - if (!(uri.getScheme().equals("camli") && uri.getHost().equals("settings"))) { - Toast.makeText(this, "QR code not a camli://settings/ URL", Toast.LENGTH_LONG).show(); + if (!(uri.getScheme().equals("perkeep") && uri.getHost().equals("settings"))) { + Toast.makeText(this, "QR code not a perkeep://settings/ URL", Toast.LENGTH_LONG).show(); return; } @@ -230,21 +226,16 @@ public class SettingsActivity extends PreferenceActivity { @Override protected void onPause() { super.onPause(); - mSharedPrefs - .unregisterOnSharedPreferenceChangeListener(prefChangedHandler); - if (mServiceConnection != null) { - unbindService(mServiceConnection); - } + mSharedPrefs.unregisterOnSharedPreferenceChangeListener(prefChangedHandler); + unbindService(mServiceConnection); } @Override protected void onResume() { super.onResume(); updatePreferenceSummaries(); - mSharedPrefs - .registerOnSharedPreferenceChangeListener(prefChangedHandler); - bindService(new Intent(this, UploadService.class), mServiceConnection, - Context.BIND_AUTO_CREATE); + mSharedPrefs.registerOnSharedPreferenceChangeListener(prefChangedHandler); + bindService(new Intent(this, UploadService.class), mServiceConnection, Context.BIND_AUTO_CREATE); } private void updatePreferenceSummaries() { @@ -265,8 +256,7 @@ public class SettingsActivity extends PreferenceActivity { WifiInfo wifiInfo = wifiManager.getConnectionInfo(); if (wifiInfo != null) { int ip = wifiInfo.getIpAddress(); - value = String.format("%d.%d.%d.", ip & 0xff, (ip >> 8) & 0xff, - (ip >> 16) & 0xff) + value; + value = String.format("%d.%d.%d.", ip & 0xff, (ip >> 8) & 0xff, (ip >> 16) & 0xff) + value; devIPPref.setText(value); mPrefs.setDevIP(value); } @@ -277,8 +267,7 @@ public class SettingsActivity extends PreferenceActivity { usernamePref.setEnabled(enabled); passwordPref.setEnabled(enabled); if (!enabled) { - devIPPref.setSummary("Using http://" + value - + ":3179 user/pass \"camlistore\", \"pass3179\""); + devIPPref.setSummary("Using http://" + value + ":3179 user/pass \"perkeep\", \"pass3179\""); } else { devIPPref.setSummary("(Dev-server IP to override settings above)"); } diff --git a/clients/android/app/src/main/java/org/camlistore/UploadApplication.java b/clients/android/app/src/main/java/org/camlistore/UploadApplication.java index c7ea3c871..ea009fd46 100644 --- a/clients/android/app/src/main/java/org/camlistore/UploadApplication.java +++ b/clients/android/app/src/main/java/org/camlistore/UploadApplication.java @@ -16,76 +16,24 @@ limitations under the License. package org.camlistore; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.Scanner; import android.app.Application; -import android.content.pm.PackageManager.NameNotFoundException; import android.util.Log; + public class UploadApplication extends Application { private final static String TAG = "UploadApplication"; private final static boolean STRICT_MODE = true; - private long getAPKModTime() { - try { - return getPackageManager().getPackageInfo(getPackageName(), 0).lastUpdateTime; - } catch (NameNotFoundException e) { - throw new RuntimeException(e); - } - } - - private void copyGoBinary() { - long myTime = getAPKModTime(); - String dstFile = getBaseContext().getFilesDir().getAbsolutePath() + "/pk-put.bin"; - File f = new File(dstFile); - Log.d(TAG, " My Time: " + myTime); - Log.d(TAG, "Bin Time: " + f.lastModified()); - if (f.exists() && f.lastModified() > myTime) { - Log.d(TAG, "Go binary modtime up-to-date."); - return; - } - Log.d(TAG, "Go binary missing or modtime stale. Re-copying from APK."); - try { - InputStream is = getAssets().open("pk-put.arm"); - FileOutputStream fos = getBaseContext().openFileOutput("pk-put.bin.writing", MODE_PRIVATE); - byte[] buf = new byte[8192]; - int offset; - while ((offset = is.read(buf)) > 0) { - fos.write(buf, 0, offset); - } - is.close(); - fos.flush(); - // Make sure that all data is written before rename by calling fsync (ext4 file system) - fos.getFD().sync(); - fos.close(); - - String writingFilePath = dstFile + ".writing"; - Log.d(TAG, "wrote out " + writingFilePath); - f = new File(writingFilePath); - f.setLastModified(myTime); - Log.d(TAG, "set modtime of " + writingFilePath); - f.setExecutable(true); - Log.d(TAG, "made " + writingFilePath + " executable"); - - f.renameTo(new File(dstFile)); - Log.d(TAG, "moved " + writingFilePath + " to " + dstFile); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - @Override public void onCreate() { super.onCreate(); - copyGoBinary(); - if (!STRICT_MODE) { Log.d(TAG, "Starting UploadApplication; release build."); return; @@ -98,35 +46,33 @@ public class UploadApplication extends Application { } try { - Class strictmode = Class.forName("android.os.StrictMode"); + Class strictmode = Class.forName("android.os.StrictMode"); Log.d(TAG, "StrictMode class found."); Method method = strictmode.getMethod("enableDefaults"); Log.d(TAG, "enableDefaults method found."); method.invoke(null); - } catch (ClassNotFoundException e) { - } catch (LinkageError e) { - } catch (IllegalAccessException e) { - } catch (NoSuchMethodException e) { - } catch (SecurityException e) { - } catch (java.lang.reflect.InvocationTargetException e) { + } catch (ClassNotFoundException | LinkageError | IllegalAccessException | NoSuchMethodException | SecurityException | InvocationTargetException ignored) { } } - public String getCamputVersion() { - InputStream is = null; + private String getPkBin() { + return getApplicationInfo().nativeLibraryDir + "/libpkput.so"; + } + + public String getPkPutVersion() { + String prefix = getPkBin() + " version:"; try { - is = getAssets().open("pk-put-version.txt"); - BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); - return br.readLine(); + ProcessBuilder pb = new ProcessBuilder(); + pb.command(getPkBin(), "-version"); + pb.redirectErrorStream(true); + Scanner scanner = new java.util.Scanner(new InputStreamReader(pb.start().getInputStream())).useDelimiter("\\A"); + String versionOutput = scanner.hasNext() ? scanner.next() : ""; + if (versionOutput.startsWith(prefix)) { + return versionOutput.substring(prefix.length()); + } + return versionOutput; } catch (IOException e) { return e.toString(); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - } - } } } } diff --git a/clients/android/app/src/main/java/org/camlistore/UploadService.java b/clients/android/app/src/main/java/org/camlistore/UploadService.java index def4a06cc..90cf75f8e 100644 --- a/clients/android/app/src/main/java/org/camlistore/UploadService.java +++ b/clients/android/app/src/main/java/org/camlistore/UploadService.java @@ -35,15 +35,14 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.app.TaskStackBuilder; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.net.wifi.WifiManager; -import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.os.FileObserver; import android.os.IBinder; import android.os.ParcelFileDescriptor; @@ -51,15 +50,13 @@ import android.os.Parcelable; import android.os.PowerManager; import android.os.RemoteException; import android.provider.MediaStore; -import android.support.v4.app.TaskStackBuilder; import android.util.Log; -import android.widget.Toast; public class UploadService extends Service { private static final String TAG = "UploadService"; - private static int NOTIFY_ID_UPLOADING = 0x001; - private static int NOTIFY_ID_FOREGROUND = 0x002; + private static final int NOTIFY_ID_UPLOADING = 0x001; + private static final int NOTIFY_ID_FOREGROUND = 0x002; public static final String INTENT_POWER_CONNECTED = "POWER_CONNECTED"; public static final String INTENT_POWER_DISCONNECTED = "POWER_DISCONNECTED"; @@ -68,17 +65,13 @@ public class UploadService extends Service { public static final String INTENT_NETWORK_NOT_WIFI = "NOT_WIFI_NOW"; // Everything in this block guarded by 'this': - private boolean mUploading = false; // user's desired state (notified - // quickly) - private UploadThread mUploadThread = null; // last thread created; null when - // thread exits - private Notification.Builder mNotificationBuilder; // null until upload is - // started/resumed - private NotificationChannel mNotificationChannel; + private boolean mUploading = false; // user's desired state (notified quickly) + private UploadThread mUploadThread = null; // last thread created; null when thread exits + private Notification.Builder mNotificationBuilder; // null until upload is started/resumed private int mLastNotificationProgress = 0; // last computed value of the uploaded bytes, to avoid excessive notification updates - private final Map mFileBytesRemain = new HashMap(); - private final LinkedList mQueueList = new LinkedList(); - private final Map mStatValue = new TreeMap(); + private final Map mFileBytesRemain = new HashMap<>(); + private final LinkedList mQueueList = new LinkedList<>(); + private final Map mStatValue = new TreeMap<>(); private IStatusCallback mCallback = DummyNullCallback.instance(); private String mLastUploadStatusText = null; // single line private String mLastUploadStatsText = null; // multi-line stats @@ -131,21 +124,17 @@ public class UploadService extends Service { stackBuilder.addParentStack(SettingsActivity.class); // Adds the Intent that starts the Activity to the top of the stack stackBuilder.addNextIntent(notificationIntent); - PendingIntent pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE|PendingIntent.FLAG_UPDATE_CURRENT); - // Create the NotificationChannel, but only on API 26+ because - // the NotificationChannel class is new and not in the support library - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - mNotificationChannel = new NotificationChannel(getString(R.string.channel_id), - getText(R.string.channel_name), NotificationManager.IMPORTANCE_DEFAULT); - mNotificationChannel.setDescription(getString(R.string.channel_description)); - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - mNotificationManager.createNotificationChannel(mNotificationChannel); - autoUploadNotif = new Notification.Builder(this, getString(R.string.channel_id)); - } else { - autoUploadNotif = new Notification.Builder(this); - } + NotificationChannel mNotificationChannel = new NotificationChannel( + getString(R.string.channel_id), + getText(R.string.channel_name), + NotificationManager.IMPORTANCE_DEFAULT); + mNotificationChannel.setDescription(getString(R.string.channel_description)); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + mNotificationManager.createNotificationChannel(mNotificationChannel); + autoUploadNotif = new Notification.Builder(this, getString(R.string.channel_id)); autoUploadNotif.setContentTitle(getText(R.string.notification_title)) .setContentText(notificationMessage()) .setSmallIcon(R.drawable.ic_stat_notify) @@ -176,16 +165,16 @@ public class UploadService extends Service { startService(new Intent(UploadService.this, UploadService.class)); } - // This is @Override as of SDK version 5, but we're targeting 4 (Android - // 1.6) - private static final int START_STICKY = 1; // in SDK 5 - @Override public int onStartCommand(Intent intent, int flags, int startId) { handleCommand(intent); // We want this service to continue running until it is explicitly // stopped, so return sticky. - return START_STICKY; + return Service.START_STICKY; + } + + private String getPkBin() { + return getApplicationInfo().nativeLibraryDir + "/libpkput.so"; } private void handleCommand(Intent intent) { @@ -273,62 +262,55 @@ public class UploadService extends Service { } final Uri uri = (Uri) streamValue; - Util.runAsync(new Runnable() { - @Override - public void run() { - try { - service.enqueueUpload(uri); - } catch (RemoteException e) { - } + Util.runAsync(() -> { + try { + service.enqueueUpload(uri); + } catch (RemoteException ignored) { } }); } private void handleUploadAll() { startService(new Intent(UploadService.this, UploadService.class)); - final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Camli Upload All"); + final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PerkeepUploadService:UploadAll"); wakeLock.acquire(); - Util.runAsync(new Runnable() { - @Override - public void run() { - try { - List dirs = getBackupDirs(); - List filesToQueue = new ArrayList(); - for (String dirName : dirs) { - File dir = new File(dirName); - if (!dir.exists()) { - continue; - } - Log.d(TAG, "Uploading all in directory: " + dirName); - File[] files = dir.listFiles(); - if (files != null) { - for (int i = 0; i < files.length; ++i) { - File f = files[i]; - if (f.isDirectory()) { - // Skip thumbnails directory. - // TODO: are any interesting enough to recurse into? - // Definitely don't need to upload thumbnails, but - // but maybe some other app in the the future creates - // sharded directories. Eye-Fi doesn't, though. - continue; - } - filesToQueue.add(Uri.fromFile(f)); + Util.runAsync(() -> { + try { + List dirs = getBackupDirs(); + List filesToQueue = new ArrayList<>(); + for (String dirName : dirs) { + File dir = new File(dirName); + if (!dir.exists()) { + continue; + } + Log.d(TAG, "Uploading all in directory: " + dirName); + File[] files = dir.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) { + // Skip thumbnails directory. + // TODO: are any interesting enough to recurse into? + // Definitely don't need to upload thumbnails, but + // but maybe some other app in the the future creates + // sharded directories. Eye-Fi doesn't, though. + continue; } + filesToQueue.add(Uri.fromFile(f)); } } - try { - service.enqueueUploadList(filesToQueue); - } catch (RemoteException e) { - } - } finally { - wakeLock.release(); } + try { + service.enqueueUploadList(filesToQueue); + } catch (RemoteException ignored) { + } + } finally { + wakeLock.release(); } }); } private List getBackupDirs() { - ArrayList dirs = new ArrayList(); + ArrayList dirs = new ArrayList<>(); String stripped = "/Android/data/org.camlistore/files"; // We use getExternalFilesDirs instead of getExternalStorageDirectory, so we can // try both the emulated SD card (the filesystem on the internal memory really), @@ -337,6 +319,7 @@ public class UploadService extends Service { String dirPath = dirName.getAbsolutePath(); String root = dirPath.substring(0, dirPath.indexOf(stripped)); if (mPrefs.autoDirPhotos()) { + dirs.add(root + "/Pictures"); dirs.add(root + "/DCIM/Camera"); dirs.add(root + "/DCIM/100MEDIA"); dirs.add(root + "/DCIM/100ANDRO"); @@ -353,7 +336,7 @@ public class UploadService extends Service { private void handleSendMultiple(Intent intent) { ArrayList items = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - ArrayList uris = new ArrayList(items.size()); + ArrayList uris = new ArrayList<>(items.size()); for (Parcelable p : items) { if (!(p instanceof Uri)) { Log.d(TAG, "uh, unknown thing " + p); @@ -362,13 +345,10 @@ public class UploadService extends Service { uris.add((Uri) p); } final ArrayList finalUris = uris; - Util.runAsync(new Runnable() { - @Override - public void run() { - try { - service.enqueueUploadList(finalUris); - } catch (RemoteException e) { - } + Util.runAsync(() -> { + try { + service.enqueueUploadList(finalUris); + } catch (RemoteException ignored) { } }); } @@ -397,27 +377,8 @@ public class UploadService extends Service { private void startBackgroundWatchers() { Log.d(TAG, "Starting background watchers..."); synchronized (UploadService.this) { - maybeAddObserver("DCIM/Camera"); - maybeAddObserver("DCIM/100MEDIA"); - maybeAddObserver("DCIM/100ANDRO"); - maybeAddObserver("DCIM/CardboardCamera"); - maybeAddObserver("Eye-Fi"); - maybeAddObserver("gpx"); - } - } - - // Requires that UploadService.this is locked. - private void maybeAddObserver(String suffix) { - String stripped = "Android/data/org.camlistore/files"; - // We use getExternalFilesDirs instead of getExternalStorageDirectory, so we can - // try both the emulated SD card (the filesystem on the internal memory really), - // and any existing SD card as well. - for (File dirName : getExternalFilesDirs(null)) { - String dirPath = dirName.getAbsolutePath(); - String root = dirPath.substring(0, dirPath.indexOf(stripped)); - File f = new File(root, suffix); - if (f.exists()) { - mObservers.add(new CamliFileObserver(service, f)); + for (String dir: getBackupDirs()) { + mObservers.add(new PerkeepFileObserver(service, new File(dir))); } } } @@ -425,7 +386,7 @@ public class UploadService extends Service { @Override public void onDestroy() { synchronized (this) { - Log.d(TAG, "onDestroy of camli UploadService; thread=" + mUploadThread + "; uploading=" + mUploading + "; queue size=" + mFileBytesRemain.size()); + Log.d(TAG, "onDestroy of perkeep UploadService; thread=" + mUploadThread + "; uploading=" + mUploading + "; queue size=" + mFileBytesRemain.size()); } super.onDestroy(); if (mUploadThread != null) { @@ -439,9 +400,7 @@ public class UploadService extends Service { // LinkedList. Doesn't return null. LinkedList uploadQueue() { synchronized (this) { - LinkedList copy = new LinkedList(); - copy.addAll(mQueueList); - return copy; + return new LinkedList<>(mQueueList); } } @@ -453,41 +412,30 @@ public class UploadService extends Service { } try { cb.setUploadStatusText(status); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } } - void setInFlightBytes(int v) { - synchronized (this) { - mBytesInFlight = v; - } - broadcastByteStatus(); - } - void broadcastByteStatus() { - Notification notification = null; synchronized (this) { - if (mNotificationBuilder != null) { - int progress = (int)(100 * (double)mBytesUploaded/(double)mBytesTotal); + if (mNotificationBuilder == null) { + return; + } + int progress = (int)(100 * (double)mBytesUploaded/(double)mBytesTotal); - // Only build new notification when progress value actually changes. Some - // devices slow down and finally freeze completely when updating too often. - if (mLastNotificationProgress != progress) { - mLastNotificationProgress = progress; + // Only build new notification when progress value actually changes. Some + // devices slow down and finally freeze completely when updating too often. + if (mLastNotificationProgress != progress) { + mLastNotificationProgress = progress; - mNotificationBuilder.setProgress(100, progress, false); - notification = mNotificationBuilder.build(); - } + mNotificationBuilder.setProgress(100, progress, false); + mNotificationManager.notify(NOTIFY_ID_UPLOADING, mNotificationBuilder.build()); } try { mCallback.setByteStatus(mBytesUploaded, mBytesInFlight, mBytesTotal); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } } - - if (notification != null) { - mNotificationManager.notify(NOTIFY_ID_UPLOADING, notification); - } } void broadcastFileStatus() { @@ -495,7 +443,7 @@ public class UploadService extends Service { synchronized (this) { try { mCallback.setFileStatus(mFilesUploaded, mFilesInFlight, mFilesTotal); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } } } @@ -506,7 +454,7 @@ public class UploadService extends Service { mCallback.setUploading(mUploading); mCallback.setUploadStatusText(mLastUploadStatusText); mCallback.setUploadStatsText(mLastUploadStatsText); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } } broadcastFileStatus(); @@ -521,14 +469,14 @@ public class UploadService extends Service { mUploading = false; try { mCallback.setUploading(false); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } } } /** * Callback from the UploadThread to the service. - * + * * @param qf * the queued file that was successfully uploaded. */ @@ -563,7 +511,7 @@ public class UploadService extends Service { synchronized (this) { Long remain = mFileBytesRemain.get(qf); if (remain != null) { - long actual = Math.min(size, remain.longValue()); + long actual = Math.min(size, remain); mBytesUploaded += actual; mFileBytesRemain.put(qf, remain - actual); } @@ -578,22 +526,28 @@ public class UploadService extends Service { stopSelf(); } else { Log.d(TAG, "stopServiceIfEmpty; NOT stopping; " + mFileBytesRemain.isEmpty() + "; " + mUploading + "; " + (mUploadThread != null)); - return; } } } ParcelFileDescriptor getFileDescriptor(Uri uri) { + // short race between inotify and the content resolver; retry a few times with a short sleep ContentResolver cr = getContentResolver(); try { - return cr.openFileDescriptor(uri, "r"); - } catch (FileNotFoundException e) { - Log.w(TAG, "FileNotFound in getFileDescriptor() for " + uri); - return null; - } + for (int i = 0; i < 2; i++) { + try { + return cr.openFileDescriptor(uri, "r"); + } catch (FileNotFoundException e) { + Log.w(TAG, "FileNotFound in getFileDescriptor() for " + uri); + } + Thread.sleep(500); + } + } catch (InterruptedException ignored){} + + return null; } - private void incrementFilesToUpload(int size) throws RemoteException { + private void incrementFilesToUpload(int size) { synchronized (UploadService.this) { mFilesTotal += size; } @@ -610,19 +564,13 @@ public class UploadService extends Service { return uri.getPath(); } String[] proj = { MediaStore.Images.Media.DATA }; - Cursor cursor = null; - try { - cursor = getContentResolver().query(uri, proj, null, null, null); + try (Cursor cursor = getContentResolver().query(uri, proj, null, null, null)) { if (cursor == null) { return null; } cursor.moveToFirst(); int columnIndex = cursor.getColumnIndex(proj[0]); return cursor.getString(columnIndex); // might still be null - } finally { - if (cursor != null) { - cursor.close(); - } } } @@ -649,7 +597,7 @@ public class UploadService extends Service { } private boolean enqueueSingleUri(Uri uri) throws RemoteException { - long statSize = 0; + long statSize; { ParcelFileDescriptor pfd = getFileDescriptor(uri); if (pfd == null) { @@ -662,7 +610,7 @@ public class UploadService extends Service { } finally { try { pfd.close(); - } catch (IOException e) { + } catch (IOException ignored) { } } } @@ -676,7 +624,7 @@ public class UploadService extends Service { QueuedFile qf = new QueuedFile(uri, statSize, diskPath); - boolean needResume = false; + boolean needResume; synchronized (UploadService.this) { if (mFileBytesRemain.containsKey(qf)) { Log.d(TAG, "Dup blob enqueue, ignoring " + qf); @@ -709,14 +657,14 @@ public class UploadService extends Service { } @Override - public boolean isUploading() throws RemoteException { + public boolean isUploading() { synchronized (UploadService.this) { return mUploading; } } @Override - public void registerCallback(IStatusCallback cb) throws RemoteException { + public void registerCallback(IStatusCallback cb) { // TODO: permit multiple listeners? when need comes. synchronized (UploadService.this) { if (cb == null) { @@ -728,7 +676,7 @@ public class UploadService extends Service { } @Override - public void unregisterCallback(IStatusCallback cb) throws RemoteException { + public void unregisterCallback(IStatusCallback cb) { synchronized (UploadService.this) { mCallback = DummyNullCallback.instance(); } @@ -743,8 +691,8 @@ public class UploadService extends Service { return false; } - final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Camli Upload"); - final WifiManager.WifiLock wifiLock = mWifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "Camli Upload"); + final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PerkeepUploadService:resume"); + final WifiManager.WifiLock wifiLock = mWifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "PerkeepUploadService:resume"); synchronized (UploadService.this) { if (mUploadThread != null) { @@ -758,13 +706,13 @@ public class UploadService extends Service { mNotificationBuilder = new Notification.Builder(UploadService.this); mNotificationBuilder.setOngoing(true) .setContentTitle("Uploading") - .setContentText("Camlistore uploader running") + .setContentText("perkeep uploader running") .setSmallIcon(android.R.drawable.stat_sys_upload); mNotificationManager.notify(NOTIFY_ID_UPLOADING, mNotificationBuilder.build()); mLastNotificationProgress = -1; mUploading = true; - mUploadThread = new UploadThread(UploadService.this, hp, mPrefs.username(), mPrefs.password()); + mUploadThread = new UploadThread(UploadService.this, hp, mPrefs.username(), mPrefs.password(), getPkBin()); mUploadThread.start(); // Start a thread to release the wakelock... @@ -801,7 +749,7 @@ public class UploadService extends Service { } @Override - public boolean pause() throws RemoteException { + public boolean pause() { synchronized (UploadService.this) { if (mUploadThread != null) { stopUploadThread(); @@ -812,14 +760,14 @@ public class UploadService extends Service { } @Override - public int queueSize() throws RemoteException { + public int queueSize() { synchronized (UploadService.this) { return mQueueList.size(); } } @Override - public void stopEverything() throws RemoteException { + public void stopEverything() { synchronized (UploadService.this) { mNotificationManager.cancel(NOTIFY_ID_UPLOADING); mFileBytesRemain.clear(); @@ -837,7 +785,7 @@ public class UploadService extends Service { } @Override - public void setBackgroundWatchersEnabled(boolean enabled) throws RemoteException { + public void setBackgroundWatchersEnabled(boolean enabled) { if (enabled) { startUploadService(); UploadService.this.stopBackgroundWatchers(); @@ -849,7 +797,7 @@ public class UploadService extends Service { mNotificationManager.notify(NOTIFY_ID_FOREGROUND, notif); } - public void reloadSettings() throws RemoteException { + public void reloadSettings() { String profileName = Preferences.filename(UploadService.this.getBaseContext()); Log.d(TAG, "reloading settings from: " + profileName); synchronized (UploadService.this) { @@ -891,7 +839,7 @@ public class UploadService extends Service { } try { mCallback.setUploadStatsText(v); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } } @@ -903,7 +851,7 @@ public class UploadService extends Service { mUploadThread = null; try { mCallback.setUploading(false); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } } mUploading = false; @@ -923,7 +871,7 @@ public class UploadService extends Service { public void onUploadErrors(String errors) { try { mCallback.setUploadErrorsText(errors); - } catch (RemoteException e) { + } catch (RemoteException ignored) { } } } diff --git a/clients/android/app/src/main/java/org/camlistore/UploadThread.java b/clients/android/app/src/main/java/org/camlistore/UploadThread.java index ea9cadc59..57d005aa1 100644 --- a/clients/android/app/src/main/java/org/camlistore/UploadThread.java +++ b/clients/android/app/src/main/java/org/camlistore/UploadThread.java @@ -21,11 +21,11 @@ import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; -import java.util.ListIterator; +import java.util.Objects; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -41,23 +41,21 @@ public class UploadThread extends Thread { private final HostPort mHostPort; private final String mUsername; private final String mPassword; - private final LinkedBlockingQueue msgCh = new LinkedBlockingQueue(); + private final String mPkPut; + private final LinkedBlockingQueue msgCh = new LinkedBlockingQueue<>(); - AtomicReference goProcess = new AtomicReference(); - AtomicReference toChildRef = new AtomicReference(); - HashMap mQueuedFile = new HashMap(); // guarded - // by - // itself + AtomicReference goProcess = new AtomicReference<>(); + final HashMap mQueuedFile = new HashMap<>(); // guarded by itself - private final Object stdinLock = new Object(); // guards setting and writing - // to stdinWriter + private final Object stdinLock = new Object(); // guards setting and writing to stdinWriter private BufferedWriter stdinWriter; - public UploadThread(UploadService uploadService, HostPort hp, String username, String password) { + public UploadThread(UploadService uploadService, HostPort hp, String username, String password, String pkput) { mService = uploadService; mHostPort = hp; mUsername = username; mPassword = password; + mPkPut = pkput; } public void stopUploads() { @@ -83,22 +81,15 @@ public class UploadThread extends Thread { } // Unnecessary paranoia, never seen in practice: - new Thread() { - @Override - public void run() { - try { - Thread.sleep(750, 0); - stopUploads(); // force kill if still alive. - } catch (InterruptedException e) { - } - + new Thread(() -> { + try { + Thread.sleep(750, 0); + stopUploads(); // force kill if still alive. + } catch (InterruptedException ignored) { } - }.start(); - } - } - private String binaryPath(String suffix) { - return mService.getBaseContext().getFilesDir().getAbsolutePath() + "/" + suffix; + }).start(); + } } private void status(String st) { @@ -120,15 +111,15 @@ public class UploadThread extends Thread { } } - public boolean enqueueFile(QueuedFile qf) { + public void enqueueFile(QueuedFile qf) { String diskPath = qf.getDiskPath(); if (diskPath == null) { Log.d(TAG, "file has no disk path: " + qf); - return false; + return; } synchronized (stdinLock) { if (stdinWriter == null) { - return false; + return; } synchronized (mQueuedFile) { mQueuedFile.put(diskPath, qf); @@ -138,10 +129,8 @@ public class UploadThread extends Thread { stdinWriter.flush(); } catch (IOException e) { Log.d(TAG, "Failed to write " + diskPath + " to pk-put stdin: " + e); - return false; } } - return true; } @Override @@ -155,10 +144,10 @@ public class UploadThread extends Thread { mService.onStatReceived(null, 0); - Process process = null; + Process process; try { ProcessBuilder pb = new ProcessBuilder(); - pb.command(binaryPath("pk-put.bin"), "--server=" + mHostPort.urlPrefix(), "file", "-stdinargs", "-vivify"); + pb.command(mPkPut, "--server=" + mHostPort.urlPrefix(), "file", "-stdinargs", "-vivify"); pb.redirectErrorStream(false); pb.environment().put("CAMLI_AUTH", "userpass:" + mUsername + ":" + mPassword); pb.environment().put("CAMLI_CACHE_DIR", mService.getCacheDir().getAbsolutePath()); @@ -166,7 +155,7 @@ public class UploadThread extends Thread { process = pb.start(); goProcess.set(process); synchronized (stdinLock) { - stdinWriter = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), "UTF-8")); + stdinWriter = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8)); } new CopyToAndroidLogThread("stderr", process.getErrorStream(), mService).start(); new ParseCamputOutputThread(process, mService).start(); @@ -175,14 +164,13 @@ public class UploadThread extends Thread { throw new RuntimeException(e); } - ListIterator iter = mService.uploadQueue().listIterator(); - while (iter.hasNext()) { - enqueueFile(iter.next()); + for (QueuedFile queuedFile : mService.uploadQueue()) { + enqueueFile(queuedFile); } // Loop forever reading from msgCh while (true) { - UploadThreadMessage msg = null; + UploadThreadMessage msg; try { msg = msgCh.poll(10, TimeUnit.SECONDS); } catch (InterruptedException e) { @@ -209,7 +197,7 @@ public class UploadThread extends Thread { if (!m.matches()) { throw new RuntimeException("bogus CamputChunkMessage: " + line); } - mSize = Long.parseLong(m.group(1)); + mSize = Long.parseLong(Objects.requireNonNull(m.group(1))); mFilename = m.group(3); } @@ -227,7 +215,7 @@ public class UploadThread extends Thread { // STAT %s %d\n private final static Pattern statPattern = Pattern.compile("^STAT (\\S+) (\\d+)\\b"); - public class CamputStatMessage { + public static class CamputStatMessage { private final Matcher mm; public CamputStatMessage(String line) { @@ -242,14 +230,14 @@ public class UploadThread extends Thread { } public long value() { - return Long.parseLong(mm.group(2)); + return Long.parseLong(Objects.requireNonNull(mm.group(2))); } } // STATS nfile=%d nbyte=%d skfile=%d skbyte=%d upfile=%d upbyte=%d\n private final static Pattern statsPattern = Pattern.compile("^STATS nfile=(\\d+) nbyte=(\\d+) skfile=(\\d+) skbyte=(\\d+) upfile=(\\d+) upbyte=(\\d+)"); - public class CamputStatsMessage { + public static class CamputStatsMessage { private final Matcher mm; public CamputStatsMessage(String line) { @@ -260,7 +248,7 @@ public class UploadThread extends Thread { } private long field(int n) { - return Long.parseLong(mm.group(n)); + return Long.parseLong(Objects.requireNonNull(mm.group(n))); } public long totalFiles() { @@ -302,11 +290,11 @@ public class UploadThread extends Thread { @Override public void run() { while (true) { - String line = null; + String line; try { line = mBufIn.readLine(); } catch (IOException e) { - Log.d(TAG, "Exception reading pk-put's stdout: " + e.toString()); + Log.d(TAG, "Exception reading pk-put's stdout: " + e); return; } if (line == null) { @@ -333,7 +321,7 @@ public class UploadThread extends Thread { } if (line.startsWith("FILE_UPLOADED ")) { String filename = line.substring(14).trim(); - QueuedFile qf = null; + QueuedFile qf; synchronized (mQueuedFile) { qf = mQueuedFile.get(filename); if (qf != null) { @@ -381,7 +369,7 @@ public class UploadThread extends Thread { private final BufferedReader mBufIn; private final UploadService mService; private final String mTag; - private final ArrayList mLines = new ArrayList(); + private final ArrayList mLines = new ArrayList<>(); public CopyToAndroidLogThread(String stream, InputStream in, UploadService service) { mBufIn = new BufferedReader(new InputStreamReader(in)); @@ -392,11 +380,11 @@ public class UploadThread extends Thread { @Override public void run() { while (true) { - String line = null; + String line; try { line = mBufIn.readLine(); } catch (IOException e) { - Log.d(mTag, "Exception: " + e.toString()); + Log.d(mTag, "Exception: " + e); return; } if (line == null) { diff --git a/clients/android/app/src/main/java/org/camlistore/Util.java b/clients/android/app/src/main/java/org/camlistore/Util.java index ec8686dd4..3a1344ea6 100644 --- a/clients/android/app/src/main/java/org/camlistore/Util.java +++ b/clients/android/app/src/main/java/org/camlistore/Util.java @@ -16,134 +16,14 @@ limitations under the License. package org.camlistore; -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.concurrent.locks.ReentrantLock; - -import android.os.AsyncTask; -import android.os.Looper; -import android.util.Base64; -import android.util.Log; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class Util { - private static final String TAG = "Camli_Util"; - - public static String slurp(InputStream in) throws IOException { - StringBuilder sb = new StringBuilder(); - byte[] b = new byte[4096]; - for (int n; (n = in.read(b)) != -1;) { - sb.append(new String(b, 0, n)); - } - return sb.toString(); - } - - public static byte[] slurpToByteArray(InputStream inputStream) throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - byte[] buffer = new byte[4096]; - for (int numRead; (numRead = inputStream.read(buffer)) != -1;) { - outputStream.write(buffer, 0, numRead); - } - return outputStream.toByteArray(); - } - - public static void copyFile(File fromFile, File toFile) throws IOException { - FileInputStream inputStream = new FileInputStream(fromFile); - FileOutputStream outputStream = new FileOutputStream(toFile); - byte[] buffer = new byte[4096]; - for (int numRead; (numRead = inputStream.read(buffer)) != -1;) - outputStream.write(buffer, 0, numRead); - inputStream.close(); - outputStream.close(); - } + private static final int NUM_THREADS = 4; + private static final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS); public static void runAsync(final Runnable r) { - new AsyncTask() { - @Override - protected Void doInBackground(Void... unused) { - r.run(); - return null; - } - }.execute(); - } - - public static boolean onMainThread() { - return Looper.myLooper() == Looper.getMainLooper(); - } - - public static void assertMainThread() { - if (!onMainThread()) { - throw new RuntimeException("Assert: unexpected call off the main thread"); - } - } - - public static void assertNotMainThread() { - if (onMainThread()) { - throw new RuntimeException("Assert: unexpected call on main thread"); - } - } - - // Asserts that |lock| is held by the current thread. - public static void assertLockIsHeld(ReentrantLock lock) { - if (!lock.isHeldByCurrentThread()) { - throw new RuntimeException("Assert: mandatory lock isn't held by current thread"); - } - } - - // Asserts that |lock| is not held by the current thread. - public static void assertLockIsNotHeld(ReentrantLock lock) { - if (lock.isHeldByCurrentThread()) { - throw new RuntimeException("Assert: lock is held by current thread but shouldn't be"); - } - } - - private static final String HEX = "0123456789abcdef"; - - public static String getHex(byte[] raw) { - if (raw == null) { - return null; - } - final StringBuilder hex = new StringBuilder(2 * raw.length); - for (final byte b : raw) { - hex.append(HEX.charAt((b & 0xF0) >> 4)).append( - HEX.charAt((b & 0x0F))); - } - return hex.toString(); - } - - // Requires that the fd be seeked to the beginning. - public static String getSha1(FileDescriptor fd) { - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - byte[] b = new byte[4096]; - FileInputStream fis = new FileInputStream(fd); - InputStream is = new BufferedInputStream(fis, 4096); - try { - for (int n; (n = is.read(b)) != -1;) { - md.update(b, 0, n); - } - } catch (IOException e) { - Log.w(TAG, "IOException while computing SHA-1"); - return null; - } - byte[] sha1hash = new byte[40]; - sha1hash = md.digest(); - return getHex(sha1hash); - } - - public static String getBasicAuthHeaderValue(String username, String password) { - return "Basic " + Base64.encodeToString((username + ":" + password).getBytes(), - Base64.NO_WRAP); + executor.execute(r); } } diff --git a/clients/android/app/src/main/res/xml/preferences.xml b/clients/android/app/src/main/res/xml/preferences.xml index f93be38fb..ff97e318f 100644 --- a/clients/android/app/src/main/res/xml/preferences.xml +++ b/clients/android/app/src/main/res/xml/preferences.xml @@ -3,72 +3,72 @@ android:key="first_preferencescreen" > diff --git a/clients/android/build.gradle b/clients/android/build.gradle index 0d9cfde69..a54e68137 100644 --- a/clients/android/build.gradle +++ b/clients/android/build.gradle @@ -9,8 +9,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' - + classpath 'com.android.tools.build:gradle:7.1.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/clients/android/gradle.properties b/clients/android/gradle.properties new file mode 100644 index 000000000..c96b36116 --- /dev/null +++ b/clients/android/gradle.properties @@ -0,0 +1,15 @@ +## 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. +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +#Sat Mar 12 18:44:15 CET 2022 +android.useAndroidX=true +android.enableJetifier=true diff --git a/clients/android/gradle/wrapper/gradle-wrapper.properties b/clients/android/gradle/wrapper/gradle-wrapper.properties index bb639c539..1dfffd27b 100644 --- a/clients/android/gradle/wrapper/gradle-wrapper.properties +++ b/clients/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Jun 17 01:41:24 CEST 2017 +#Sat Mar 12 17:10:42 CET 2022 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip +zipStoreBase=GRADLE_USER_HOME