android: return to android playstore

16 views
Skip to first unread message

nor...@perkeep.org

unread,
Jun 17, 2022, 10:47:47 AM6/17/22
to camlistor...@googlegroups.com


https://github.com/perkeep/perkeep/commit/1ca9f45c66c9ea03090b232993760f4eb4d758bc

commit 1ca9f45c66c9ea03090b232993760f4eb4d758bc
Author: Michael Hoffmann <mho...@posteo.de>
Date: Sat Mar 12 18:54:27 2022 +0100

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

diff --git a/clients/android/.gitignore b/clients/android/.gitignore
index 48dba23..98ffeee 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 0000000..4c3d4f9
--- /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 4075334..21d785c 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
+ compileSdkVersion 32
+
+ defaultConfig {
+ applicationId "org.camlistore"
+ minSdkVersion 26
+ targetSdkVersion 32
+ versionCode 7
+ versionName "0.11"
+ }
+
+ packagingOptions {
+ jniLibs.useLegacyPackaging = true
+ }
+
+ sourceSets {
+ main {
+ jniLibs.srcDirs = ['libs']
+ }
+ }

- // 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']
+ release {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
+ keyAlias keystoreProperties['keyAlias']
+ keyPassword keystoreProperties['keyPassword']
+
+ v1SigningEnabled true
+ v2SigningEnabled true
}
}

- 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"
- }
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 a8fb663..0000000
--- 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<CamliActivity> {
-
- public CamliActivityTest(String pkg, Class<CamliActivity> 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 45c922e..400d1e1 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 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hello">Hello World!</string>
- <string name="app_name">camlistoreTest</string>
+ <string name="app_name">perkeepTest</string>
</resources>
diff --git a/clients/android/app/src/main/AndroidManifest.xml b/clients/android/app/src/main/AndroidManifest.xml
index 11f562d..32c577d 100644
--- a/clients/android/app/src/main/AndroidManifest.xml
+++ b/clients/android/app/src/main/AndroidManifest.xml
@@ -2,31 +2,32 @@
<!-- Copyright 2017 The Perkeep Authors.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file. -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="org.camlistore"
- android:versionCode="2"
- android:versionName="0.6.1">

- <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="26" />
+<!-- We are using org.camlistore here for backwards compatibility -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.camlistore">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
- <uses-permission android:name="android.permission.BATTERY_STATS" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

- <application android:icon="@drawable/icon" android:label="@string/app_name"
- android:name=".UploadApplication" android:allowBackup="true">
+ <application android:icon="@drawable/icon"
+ android:label="@string/app_name"
+ android:name="org.camlistore.UploadApplication"
+ android:allowBackup="true">

- <service android:name=".UploadService"
+ <service android:name="org.camlistore.UploadService"
android:exported="false"
android:label="Perkeep Upload Service" />

- <activity android:name=".CamliActivity"
- android:label="@string/app_name">
+ <activity
+ android:name="org.camlistore.PerkeepActivity"
+ android:exported="true">
+
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -43,37 +44,33 @@
</intent-filter>
</activity>

- <activity android:name=".BrowseActivity">
- </activity>
+ <activity android:name="org.camlistore.SettingsActivity"/>
+
+ <activity android:name="org.camlistore.ProfilesActivity"/>

- <activity android:name=".SettingsActivity">
- </activity>
+ <receiver android:name="org.camlistore.OnBootReceiver" android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>

- <activity android:name=".ProfilesActivity">
- </activity>
-
- <receiver android:name=".OnBootReceiver">
- <intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
- </intent-filter>
- </receiver>
+ <receiver android:name="org.camlistore.OnAlarmReceiver"/>

- <receiver android:name=".OnAlarmReceiver">
- </receiver>
-
- <receiver android:name=".WifiPowerReceiver"
- android:enabled="true"
- android:priority="0">
- <intent-filter>
+ <receiver
+ android:name="org.camlistore.WifiPowerReceiver"
+ android:enabled="true"
+ android:priority="0"
+ android:exported="false">
+ <intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
- </intent-filter>
- <intent-filter>
+ </intent-filter>
+ <intent-filter>
<action android:name="android.intent.action.ACTION_POWER_CONNECTED" />
- </intent-filter>
- <intent-filter>
+ </intent-filter>
+ <intent-filter>
<action android:name="android.intent.action.ACTION_POWER_DISCONNECTED" />
- </intent-filter>
- </receiver>
+ </intent-filter>
+ </receiver>

</application>
</manifest>
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 0849c05..39a6d40 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 b96f0ed..f027a55 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 8aea559..0000000
--- 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 994a02e..0000000
--- 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 06b66ec..0000000
--- 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/CamliActivity.java b/clients/android/app/src/main/java/org/camlistore/CamliActivity.java
deleted file mode 100644
index 930e23d..0000000
--- a/clients/android/app/src/main/java/org/camlistore/CamliActivity.java
+++ /dev/null
@@ -1,414 +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.app.Activity;
-import android.app.AlertDialog;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.ServiceConnection;
-import android.content.SharedPreferences;
-import android.Manifest;
-import android.os.Bundle;
-import android.os.Handler;
-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";
-
- private static final int MENU_SETTINGS = 1;
- private static final int MENU_STOP = 2;
- private static final int MENU_STOP_DIE = 3;
- private static final int MENU_UPLOAD_ALL = 4;
- private static final int MENU_VERSION = 5;
- private static final int MENU_PROFILES = 6;
-
- private static final int READ_EXTERNAL_STORAGE_PERMISSION_RESPONSE = 0;
-
-
- private IUploadService mServiceStub = null;
- private IStatusCallback mCallback = null;
-
- // Status text update state, since it updates too quickly to do it the naive way.
- private long mLastStatusUpdate = 0; // time in millis we lasted updated the screen
- private String mStatusTextCurrent = null; // what the screen says
- private String mStatusTextWant = null; // what the service wants it to say
-
- 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 ServiceConnection mServiceConnection = new ServiceConnection() {
-
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mServiceStub = IUploadService.Stub.asInterface(service);
- Log.d(TAG, "Service connected, registering callback " + mCallback);
-
- try {
- mServiceStub.registerCallback(mCallback);
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- Log.d(TAG, "Service disconnected");
- mServiceStub = null;
- };
- };
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
-
- Looper.myQueue().addIdleHandler(mIdleHandler);
- final Button buttonToggle = (Button) 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);
-
- 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) {
- }
- }
- }
- });
-
- mCallback = new IStatusCallback.Stub() {
- private volatile int mLastBlobsUploadRemain = 0;
- private volatile int mLastBlobsDigestRemain = 0;
-
- @Override
- public void logToClient(String stuff) throws RemoteException {
- // 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.");
- }
- }
- });
- }
-
- @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());
- }
- });
- }
-
- @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);
-
- 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);
- }
- });
- }
-
- @Override
- public void setUploadStatsText(final String text) throws RemoteException {
- // 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();
- }
- }
- });
- }
-
- public void setUploadErrorsText(final String text) throws RemoteException {
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- textErrors.setText(text);
- }
- });
- }
- };
-
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- // TODO: picking files/photos to upload?
- }
-
- @Override
- protected void onDestroy() {
- // TODO Auto-generated method stub
- super.onDestroy();
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
-
- MenuItem uploadAll = menu.add(Menu.NONE, MENU_UPLOAD_ALL, 0, R.string.upload_all);
- uploadAll.setIcon(android.R.drawable.ic_menu_upload);
-
- MenuItem stop = menu.add(Menu.NONE, MENU_STOP, 0, R.string.stop);
- stop.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
-
- MenuItem stopDie = menu.add(Menu.NONE, MENU_STOP_DIE, 0, R.string.stop_die);
- stopDie.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
-
- MenuItem profiles = menu.add(Menu.NONE, MENU_PROFILES, 0, R.string.profile);
- // TODO(mpl): do we care about this icon? I don't even know where it actually appears.
- profiles.setIcon(android.R.drawable.ic_menu_preferences);
-
- MenuItem settings = menu.add(Menu.NONE, MENU_SETTINGS, 0, R.string.settings);
- settings.setIcon(android.R.drawable.ic_menu_preferences);
-
- menu.add(Menu.NONE, MENU_VERSION, 0, R.string.version);
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case MENU_STOP:
- try {
- if (mServiceStub != null) {
- mServiceStub.stopEverything();
- }
- } catch (RemoteException e) {
- // Ignore.
- }
- break;
- case MENU_STOP_DIE:
- System.exit(1);
- case MENU_SETTINGS:
- SettingsActivity.show(this);
- break;
- case MENU_PROFILES:
- ProfilesActivity.show(this);
- break;
- case MENU_VERSION:
- Toast.makeText(this, "pk-put version: " + ((UploadApplication) getApplication()).getCamputVersion(), Toast.LENGTH_LONG).show();
- break;
- case MENU_UPLOAD_ALL:
- Intent uploadAll = new Intent(UploadService.INTENT_UPLOAD_ALL);
- uploadAll.setClass(this, UploadService.class);
- Log.d(TAG, "Starting upload all...");
- startService(uploadAll);
- Log.d(TAG, "Back from upload all...");
- break;
- }
- return true;
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- try {
- if (mServiceStub != null)
- mServiceStub.unregisterCallback(mCallback);
- } catch (RemoteException e) {
- // Ignore.
- }
- if (mServiceConnection != null) {
- unbindService(mServiceConnection);
- }
- }
-
- @Override
- protected void onResume() {
- 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},
- READ_EXTERNAL_STORAGE_PERMISSION_RESPONSE);
- }
-
- SharedPreferences sp = getSharedPreferences(Preferences.filename(this.getBaseContext()), 0);
- try {
- HostPort hp = new HostPort(sp.getString(Preferences.HOST, ""));
- if (!hp.isValid()) {
- // Crashes oddly in some Android Instrumentation thing if
- // uncommented:
- // SettingsActivity.show(this);
- // return;
- }
- } catch (NumberFormatException enf) {
- AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setMessage("Server should be of form [https://]<host[:port]>")
- .setTitle("Invalid Setting");
- AlertDialog alert = builder.create();
- alert.show();
- }
-
- // Actually start the service before binding to it, so that unbinding from it does not destroy the service.
- Intent intent = getIntent();
- Intent serviceIntent = new Intent(intent);
- serviceIntent.setClass(this, UploadService.class);
- startService(serviceIntent);
- bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
-
- // TODO(mpl): maybe remove all of that below. Does the intent action still matter now?
- String action = intent.getAction();
- Log.d(TAG, "onResume; action=" + action);
-
- if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
- setIntent(new Intent(this, CamliActivity.class));
- } else {
- Log.d(TAG, "Normal CamliActivity 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.
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- Log.d(TAG, "User authorized us to read his files.");
- } else {
- // The app is useless without this permission, so we just kill ourselves.
- 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/CamliFileObserver.java
deleted file mode 100644
index da67752..0000000
--- a/clients/android/app/src/main/java/org/camlistore/CamliFileObserver.java
+++ /dev/null
@@ -1,65 +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 java.io.File;
-
-import android.net.Uri;
-import android.os.FileObserver;
-import android.os.RemoteException;
-import android.util.Log;
-
-import org.camlistore.IUploadService.Stub;
-
-public class CamliFileObserver extends FileObserver {
- private static final String TAG = "CamliFileObserver";
-
- private final File mDirectory;
- private final Stub mServiceStub;
-
- public CamliFileObserver(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
- // later).". This means that a user without, say, a "gpx/" directory
- // that then goes to "Export all Tracks.." won't start them uploading.
- mDirectory = directory;
- mServiceStub = service;
- Log.d(TAG, "Starting to watch: " + mDirectory.getAbsolutePath());
- startWatching();
- }
-
- @Override
- public void onEvent(int event, String path) {
- if (path == null) {
- // It's null for certain directory-level events.
- 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) {
- }
- }
-}
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 95e6b21..1d2fb63 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 e53bce2..9063aa8 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);
-
- alarmer.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
- SystemClock.elapsedRealtime() + 60000, AlarmManager.INTERVAL_HALF_HOUR,
+ 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,
pendingIntent);

}
diff --git a/clients/android/app/src/main/java/org/camlistore/PerkeepActivity.java b/clients/android/app/src/main/java/org/camlistore/PerkeepActivity.java
new file mode 100644
index 0000000..a55f37d
--- /dev/null
+++ b/clients/android/app/src/main/java/org/camlistore/PerkeepActivity.java
@@ -0,0 +1,380 @@
+/*
+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.app.Activity;
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.Manifest;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+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;
+ private static final int MENU_STOP_DIE = 3;
+ private static final int MENU_UPLOAD_ALL = 4;
+ private static final int MENU_VERSION = 5;
+ private static final int MENU_PROFILES = 6;
+
+ private static final int READ_EXTERNAL_STORAGE_PERMISSION_RESPONSE = 0;
+
+
+ private IUploadService mServiceStub = null;
+ private IStatusCallback mCallback = null;
+
+ // Status text update state, since it updates too quickly to do it the naive way.
+ private long mLastStatusUpdate = 0; // time in millis we lasted updated the screen
+ private String mStatusTextCurrent = null; // what the screen says
+ private String mStatusTextWant = null; // what the service wants it to say
+
+ private final Handler mHandler = new Handler();
+
+ 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() {
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mServiceStub = IUploadService.Stub.asInterface(service);
+ Log.d(TAG, "Service connected, registering callback " + mCallback);
+
+ try {
+ mServiceStub.registerCallback(mCallback);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.d(TAG, "Service disconnected");
+ mServiceStub = null;
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ Looper.myQueue().addIdleHandler(mIdleHandler);
+ final Button buttonToggle = findViewById(R.id.buttonToggle);
+
+ 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(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 final int mLastBlobsUploadRemain = 0;
+ private final int mLastBlobsDigestRemain = 0;
+
+ @Override
+ public void logToClient(String stuff) {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ 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) {
+ 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) {
+ 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());
+ });
+ }
+
+ @Override
+ public void setUploadStatusText(final String text) {
+ mHandler.post(() -> textUploadStatus.setText(text));
+ }
+
+ @Override
+ 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(() -> {
+ mStatusTextWant = text;
+ long now = System.currentTimeMillis();
+ if (mLastStatusUpdate < now - 30) {
+ mStatusTextCurrent = mStatusTextWant;
+ textStats.setText(mStatusTextWant);
+ mLastStatusUpdate = System.currentTimeMillis();
+ }
+ });
+ }
+
+ public void setUploadErrorsText(final String text) {
+ mHandler.post(() -> textErrors.setText(text));
+ }
+ };
+
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ // TODO: picking files/photos to upload?
+ }
+
+ @Override
+ protected void onDestroy() {
+ // TODO Auto-generated method stub
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ MenuItem uploadAll = menu.add(Menu.NONE, MENU_UPLOAD_ALL, 0, R.string.upload_all);
+ uploadAll.setIcon(android.R.drawable.ic_menu_upload);
+
+ MenuItem stop = menu.add(Menu.NONE, MENU_STOP, 0, R.string.stop);
+ stop.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+
+ MenuItem stopDie = menu.add(Menu.NONE, MENU_STOP_DIE, 0, R.string.stop_die);
+ stopDie.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+
+ MenuItem profiles = menu.add(Menu.NONE, MENU_PROFILES, 0, R.string.profile);
+ // TODO(mpl): do we care about this icon? I don't even know where it actually appears.
+ profiles.setIcon(android.R.drawable.ic_menu_preferences);
+
+ MenuItem settings = menu.add(Menu.NONE, MENU_SETTINGS, 0, R.string.settings);
+ settings.setIcon(android.R.drawable.ic_menu_preferences);
+
+ menu.add(Menu.NONE, MENU_VERSION, 0, R.string.version);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case MENU_STOP:
+ try {
+ if (mServiceStub != null) {
+ mServiceStub.stopEverything();
+ }
+ } catch (RemoteException e) {
+ // Ignore.
+ }
+ break;
+ case MENU_STOP_DIE:
+ System.exit(1);
+ case MENU_SETTINGS:
+ SettingsActivity.show(this);
+ break;
+ case MENU_PROFILES:
+ ProfilesActivity.show(this);
+ break;
+ case MENU_VERSION:
+ 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);
+ uploadAll.setClass(this, UploadService.class);
+ Log.d(TAG, "Starting upload all...");
+ startService(uploadAll);
+ Log.d(TAG, "Back from upload all...");
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ try {
+ if (mServiceStub != null)
+ mServiceStub.unregisterCallback(mCallback);
+ } catch (RemoteException e) {
+ // Ignore.
+ }
+ if (mServiceConnection != null) {
+ unbindService(mServiceConnection);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ 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},
+ READ_EXTERNAL_STORAGE_PERMISSION_RESPONSE);
+ }
+
+ SharedPreferences sp = getSharedPreferences(Preferences.filename(this.getBaseContext()), 0);
+ try {
+ HostPort hp = new HostPort(sp.getString(Preferences.HOST, ""));
+ if (!hp.isValid()) {
+ // Crashes oddly in some Android Instrumentation thing if
+ // uncommented:
+ // SettingsActivity.show(this);
+ // return;
+ }
+ } catch (NumberFormatException enf) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage("Server should be of form [https://]<host[:port]>")
+ .setTitle("Invalid Setting");
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ // Actually start the service before binding to it, so that unbinding from it does not destroy the service.
+ Intent intent = getIntent();
+ Intent serviceIntent = new Intent(intent);
+ serviceIntent.setClass(this, UploadService.class);
+ startService(serviceIntent);
+ bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);
+
+ // TODO(mpl): maybe remove all of that below. Does the intent action still matter now?
+ String action = intent.getAction();
+ Log.d(TAG, "onResume; action=" + action);
+
+ if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ setIntent(new Intent(this, PerkeepActivity.class));
+ } else {
+ Log.d(TAG, "Normal perkeepActivity viewing.");
+ }
+ }
+
+ @Override
+ 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 {
+ // The app is useless without this permission, so we just kill ourselves.
+ Log.d(TAG, "Permission to read files denied by user.");
+ System.exit(1);
+ }
+ }
+ }
+}
diff --git a/clients/android/app/src/main/java/org/camlistore/PerkeepFileObserver.java b/clients/android/app/src/main/java/org/camlistore/PerkeepFileObserver.java
new file mode 100644
index 0000000..13a6aaa
--- /dev/null
+++ b/clients/android/app/src/main/java/org/camlistore/PerkeepFileObserver.java
@@ -0,0 +1,74 @@
+/*
+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 java.io.File;
+import java.nio.file.Paths;
+
+import android.net.Uri;
+import android.os.FileObserver;
+import android.os.RemoteException;
+import android.util.Log;
+
+import org.camlistore.IUploadService.Stub;
+
+public class PerkeepFileObserver extends FileObserver {
+ private static final String TAG = "PerkeepFileObserver";
+
+ private final File mDirectory;
+ private final Stub mServiceStub;
+
+ 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
+ // later).". This means that a user without, say, a "gpx/" directory
+ // that then goes to "Export all Tracks.." won't start them uploading.
+ mDirectory = directory;
+ mServiceStub = service;
+ Log.d(TAG, "Starting to watch: " + mDirectory.getAbsolutePath());
+ startWatching();
+ }
+
+ @Override
+ public void onEvent(int event, String path) {
+ if (!shouldActOnEvent(path)){
+ return;
+ }
+ File fullFile = new File(mDirectory, path);
+ Log.d(TAG, "event " + event + " for " + fullFile.getAbsolutePath());
+ try {
+ 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 e3dec7a..1809a73 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 611a073..9dd161c 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<String> profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet<String>());
+ Set<String> 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<String> profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet<String>());
+ Set<String> 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 1842dd2..fc33a5c 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 f74524a..d377832 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 7a869be..34cc3c1 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);
-
- // 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
+ 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
};
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 c7ea3c8..ea009fd 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 def4a06..90cf75f 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<QueuedFile, Long> mFileBytesRemain = new HashMap<QueuedFile, Long>();
- private final LinkedList<QueuedFile> mQueueList = new LinkedList<QueuedFile>();
- private final Map<String, Long> mStatValue = new TreeMap<String, Long>();
+ private final Map<QueuedFile, Long> mFileBytesRemain = new HashMap<>();
+ private final LinkedList<QueuedFile> mQueueList = new LinkedList<>();
+ private final Map<String, Long> 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);
-
- // 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);
- }
+ PendingIntent pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE|PendingIntent.FLAG_UPDATE_CURRENT);
+
+ 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<String> dirs = getBackupDirs();
- List<Uri> filesToQueue = new ArrayList<Uri>();
- 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<String> dirs = getBackupDirs();
+ List<Uri> 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<String> getBackupDirs() {
- ArrayList<String> dirs = new ArrayList<String>();
+ ArrayList<String> 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<Parcelable> items = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- ArrayList<Uri> uris = new ArrayList<Uri>(items.size());
+ ArrayList<Uri> 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<Uri> 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<QueuedFile> uploadQueue() {
synchronized (this) {
- LinkedList<QueuedFile> copy = new LinkedList<QueuedFile>();
- 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 ea9cadc..57d005a 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<UploadThreadMessage> msgCh = new LinkedBlockingQueue<UploadThreadMessage>();
+ private final String mPkPut;
+ private final LinkedBlockingQueue<UploadThreadMessage> msgCh = new LinkedBlockingQueue<>();

- AtomicReference<Process> goProcess = new AtomicReference<Process>();
- AtomicReference<OutputStream> toChildRef = new AtomicReference<OutputStream>();
- HashMap<String, QueuedFile> mQueuedFile = new HashMap<String, QueuedFile>(); // guarded
- // by
- // itself
+ AtomicReference<Process> goProcess = new AtomicReference<>();
+ final HashMap<String, QueuedFile> 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<QueuedFile> 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<String> mLines = new ArrayList<String>();
+ private final ArrayList<String> 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 ec8686d..3a1344e 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<Void, Void, Void>() {
- @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 f93be38..ff97e31 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" >

<org.camlistore.QRPreference
- android:key="camli.qr"
+ android:key="perkeep.qr"
android:summary="@string/settings_qr_summary"
android:title="@string/settings_qr_title"/>
<EditTextPreference
- android:key="camli.host"
+ android:key="perkeep.host"
android:persistent="true"
android:summary="@string/settings_host_summary"
android:title="@string/settings_host_title" />
<EditTextPreference
- android:key="camli.username"
+ android:key="perkeep.username"
android:persistent="true"
android:title="@string/settings_username_title" />
<EditTextPreference
android:inputType="textPassword"
- android:key="camli.password"
+ android:key="perkeep.password"
android:persistent="true"
android:title="@string/settings_password_title" />

<CheckBoxPreference
- android:key="camli.auto"
+ android:key="perkeep.auto"
android:persistent="true"
android:summary="@string/settings_auto_summary"
android:title="@string/settings_auto" />

<PreferenceScreen
- android:key="camli.auto.opts"
+ android:key="perkeep.auto.opts"
android:title="Auto-upload settings" >
<CheckBoxPreference
android:defaultValue="true"
- android:key="camli.auto.photos"
+ android:key="perkeep.auto.photos"
android:persistent="true"
android:title="Photos (DCIM/Camera/)" />
<CheckBoxPreference
android:defaultValue="true"
- android:key="camli.auto.mytracks"
+ android:key="perkeep.auto.mytracks"
android:persistent="true"
android:title="MyTracks exports" />
<CheckBoxPreference
android:defaultValue="false"
- android:key="camli.auto.require_wifi"
+ android:key="perkeep.auto.require_wifi"
android:persistent="true"
android:summary="Wait for Wifi to auto-upload"
android:title="Require Wifi" />
<EditTextPreference
- android:key="camli.auto.required_wifi_ssid"
+ android:key="perkeep.auto.required_wifi_ssid"
android:persistent="true"
android:singleLine="true"
android:summary="Restrict auto-upload to this SSID"
android:title="@string/settings_auto_required_ssid" />
<CheckBoxPreference
android:defaultValue="false"
- android:key="camli.auto.require_power"
+ android:key="perkeep.auto.require_power"
android:persistent="true"
android:summary="Wait until charging to auto-upload"
android:title="Require Power" />
</PreferenceScreen>

<EditTextPreference
- android:key="camli.max_cache_mb"
+ android:key="perkeep.max_cache_mb"
android:numeric="integer"
android:persistent="true"
android:singleLine="true"
android:title="@string/settings_max_cache_size_title" />

<EditTextPreference
- android:key="camli.dev_ip"
+ android:key="perkeep.dev_ip"
android:phoneNumber="true"
android:persistent="true"
android:singleLine="true"
diff --git a/clients/android/app/src/main/res/xml/profiles.xml b/clients/android/app/src/main/res/xml/profiles.xml
index 428f690..2f17617 100644
--- a/clients/android/app/src/main/res/xml/profiles.xml
+++ b/clients/android/app/src/main/res/xml/profiles.xml
@@ -3,7 +3,7 @@
android:key="profilescreen" >

<ListPreference
- android:key="camli.profile"
+ android:key="perkeep.profile"
android:persistent="true"
android:summary="@string/profiles_summary"
android:title="@string/profiles_title"
@@ -12,7 +12,7 @@
android:defaultValue="default"/>

<EditTextPreference
- android:key="camli.newprofile"
+ android:key="perkeep.newprofile"
android:persistent="false"
android:summary="Create a new profile"
android:title="New Profile" />
diff --git a/clients/android/build.gradle b/clients/android/build.gradle
index 0d9cfde..a54e681 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 0000000..c96b361
--- /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 bb639c5..1dfffd2 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
Reply all
Reply to author
Forward
0 new messages