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
This commit is contained in:
Michael Hoffmann 2022-03-12 18:54:27 +01:00 committed by Brad Fitzpatrick
parent 5af672029e
commit 1ca9f45c66
29 changed files with 539 additions and 824 deletions

View File

@ -2,7 +2,10 @@ build
gen
bin
local.properties
keystore.properties
test/local.properties
test/build
test/gen
test/bin
app/libs
app/release

View File

@ -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)

View File

@ -5,50 +5,51 @@
*/
apply plugin: 'com.android.application'
// Create a variable called keystorePropertiesFile, and initialize it to your
// keystore.properties file, in the rootProject folder.
def keystorePropertiesFile = rootProject.file("keystore.properties")
// Initialize a new Properties() object called keystoreProperties.
def keystoreProperties = new Properties()
// Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
compileSdkVersion 27
// TODO(mpl): this should make signing the apk automatic when building the
// release flavor, but it does not seem to. figure out why. use Makefile in the
// meantime.
signingConfigs {
config {
keyAlias keystoreProperties['keyAlias']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
compileSdkVersion 32
defaultConfig {
applicationId "org.camlistore"
minSdkVersion 14
// Stay below API 26 for a while, because it deprecates the Notification Builder
// constructor we're using.
targetSdkVersion 26
// integer. used by android to prevent downgrades. not seen by user.
versionCode 4
// version shown to the user in play store.
versionName "0.10"
minSdkVersion 26
targetSdkVersion 32
versionCode 7
versionName "0.11"
}
packagingOptions {
jniLibs.useLegacyPackaging = true
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
signingConfigs {
release {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
v1SigningEnabled true
v2SigningEnabled true
}
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.release
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:26.0.0'
implementation 'com.android.support:support-compat:26.0.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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=".SettingsActivity">
</activity>
<activity android:name="org.camlistore.ProfilesActivity"/>
<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.OnBootReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".OnAlarmReceiver">
</receiver>
<receiver android:name=".WifiPowerReceiver"
android:enabled="true"
android:priority="0">
<intent-filter>
<receiver android:name="org.camlistore.OnAlarmReceiver"/>
<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>

View File

@ -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.

View File

@ -43,7 +43,7 @@ interface IUploadService {
// Stop stop uploads, clear queues.
void stopEverything();
// For the SettingsActivity
void setBackgroundWatchersEnabled(boolean enabled);

View File

@ -1,2 +0,0 @@
pk-put.arm
pk-put-version.txt

View File

@ -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

View File

@ -1 +0,0 @@
Put pk-put.arm here. It's in .gitignore because it's 6.5 MB.

View File

@ -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) {

View File

@ -25,17 +25,21 @@ import android.os.SystemClock;
import android.util.Log;
public class OnBootReceiver extends BroadcastReceiver {
private static final String TAG = "Camli_OnBootReceiver";
private static final String TAG = "perkeep_OnBootReceiver";
@Override
public void onReceive(Context context, Intent intent) {
Log.v(TAG, "onReceive on boot");
AlarmManager alarmer = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(context,
OnAlarmReceiver.class), 0);
PendingIntent pendingIntent = PendingIntent.getBroadcast(
context,
0,
new Intent(context, OnAlarmReceiver.class), PendingIntent.FLAG_IMMUTABLE);
alarmer.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 60000, AlarmManager.INTERVAL_HALF_HOUR,
alarmer.setInexactRepeating(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 60000,
AlarmManager.INTERVAL_HALF_HOUR,
pendingIntent);
}

View File

@ -31,20 +31,19 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.MessageQueue;
import android.os.RemoteException;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
public class CamliActivity extends Activity {
private static final String TAG = "CamliActivity";
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class PerkeepActivity extends Activity {
private static final String TAG = "PerkeepActivity";
private static final int MENU_SETTINGS = 1;
private static final int MENU_STOP = 2;
@ -66,17 +65,14 @@ public class CamliActivity extends Activity {
private final Handler mHandler = new Handler();
private final MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
if (mStatusTextCurrent != mStatusTextWant) {
TextView textStats = (TextView) findViewById(R.id.textStats);
mLastStatusUpdate = System.currentTimeMillis();
mStatusTextCurrent = mStatusTextWant;
textStats.setText(mStatusTextWant);
}
return true;
private final MessageQueue.IdleHandler mIdleHandler = () -> {
if (mStatusTextCurrent != mStatusTextWant) {
TextView textStats = findViewById(R.id.textStats);
mLastStatusUpdate = System.currentTimeMillis();
mStatusTextCurrent = mStatusTextWant;
textStats.setText(mStatusTextWant);
}
return true;
};
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@ -97,7 +93,7 @@ public class CamliActivity extends Activity {
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "Service disconnected");
mServiceStub = null;
};
}
};
@Override
@ -106,158 +102,131 @@ public class CamliActivity extends Activity {
setContentView(R.layout.main);
Looper.myQueue().addIdleHandler(mIdleHandler);
final Button buttonToggle = (Button) findViewById(R.id.buttonToggle);
final Button buttonToggle = findViewById(R.id.buttonToggle);
final TextView textStatus = (TextView) findViewById(R.id.textStatus);
final TextView textStats = (TextView) findViewById(R.id.textStats);
final TextView textErrors = (TextView) findViewById(R.id.textErrors);
final TextView textBlobsRemain = (TextView) findViewById(R.id.textBlobsRemain);
final TextView textUploadStatus = (TextView) findViewById(R.id.textUploadStatus);
final TextView textByteStatus = (TextView) findViewById(R.id.textByteStatus);
final ProgressBar progressBytes = (ProgressBar) findViewById(R.id.progressByteStatus);
final TextView textFileStatus = (TextView) findViewById(R.id.textFileStatus);
final ProgressBar progressFile = (ProgressBar) findViewById(R.id.progressFileStatus);
final TextView textStatus = findViewById(R.id.textStatus);
final TextView textStats = findViewById(R.id.textStats);
final TextView textErrors = findViewById(R.id.textErrors);
final TextView textBlobsRemain = findViewById(R.id.textBlobsRemain);
final TextView textUploadStatus = findViewById(R.id.textUploadStatus);
final TextView textByteStatus = findViewById(R.id.textByteStatus);
final ProgressBar progressBytes = findViewById(R.id.progressByteStatus);
final TextView textFileStatus = findViewById(R.id.textFileStatus);
final ProgressBar progressFile = findViewById(R.id.progressFileStatus);
buttonToggle.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View btn) {
Log.d(TAG, "button click! text=" + buttonToggle.getText());
if (getString(R.string.pause).equals(buttonToggle.getText())) {
try {
Log.d(TAG, "Pausing..");
mServiceStub.pause();
} catch (RemoteException e) {
}
} else if (getString(R.string.resume).equals(buttonToggle.getText())) {
try {
Log.d(TAG, "Resuming..");
mServiceStub.resume();
} catch (RemoteException e) {
}
buttonToggle.setOnClickListener(btn -> {
Log.d(TAG, "button click! text=" + buttonToggle.getText());
if (getString(R.string.pause).contentEquals(buttonToggle.getText())) {
try {
Log.d(TAG, "Pausing..");
mServiceStub.pause();
} catch (RemoteException ignored) {
}
} else if (getString(R.string.resume).contentEquals(buttonToggle.getText())) {
try {
Log.d(TAG, "Resuming..");
mServiceStub.resume();
} catch (RemoteException ignored) {
}
}
});
mCallback = new IStatusCallback.Stub() {
private volatile int mLastBlobsUploadRemain = 0;
private volatile int mLastBlobsDigestRemain = 0;
private final int mLastBlobsUploadRemain = 0;
private final int mLastBlobsDigestRemain = 0;
@Override
public void logToClient(String stuff) throws RemoteException {
public void logToClient(String stuff) {
// TODO Auto-generated method stub
}
@Override
public void setUploading(final boolean uploading) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
if (uploading) {
buttonToggle.setText(R.string.pause);
textStatus.setText(R.string.uploading);
textErrors.setText("");
} else if (mLastBlobsDigestRemain > 0) {
buttonToggle.setText(R.string.pause);
textStatus.setText(R.string.digesting);
} else {
buttonToggle.setText(R.string.resume);
int stepsRemain = mLastBlobsUploadRemain + mLastBlobsDigestRemain;
textStatus.setText(stepsRemain > 0 ? "Paused." : "Idle.");
}
public void setUploading(final boolean uploading) {
mHandler.post(() -> {
if (uploading) {
buttonToggle.setText(R.string.pause);
textStatus.setText(R.string.uploading);
textErrors.setText("");
} else if (mLastBlobsDigestRemain > 0) {
buttonToggle.setText(R.string.pause);
textStatus.setText(R.string.digesting);
} else {
buttonToggle.setText(R.string.resume);
int stepsRemain = mLastBlobsUploadRemain + mLastBlobsDigestRemain;
textStatus.setText(stepsRemain > 0 ? "Paused." : "Idle.");
}
});
}
@Override
public void setFileStatus(final int done, final int inFlight, final int total) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
boolean finished = (done == total && mLastBlobsDigestRemain == 0);
buttonToggle.setEnabled(!finished);
progressFile.setMax(total);
progressFile.setProgress(done);
progressFile.setSecondaryProgress(done + inFlight);
if (finished) {
buttonToggle.setText(getString(R.string.pause_resume));
}
StringBuilder filesUploaded = new StringBuilder(40);
if (done < 2) {
filesUploaded.append(done).append(" file uploaded");
} else {
filesUploaded.append(done).append(" files uploaded");
}
textFileStatus.setText(filesUploaded.toString());
StringBuilder sb = new StringBuilder(40);
sb.append("Files to upload: ").append(total - done);
textBlobsRemain.setText(sb.toString());
public void setFileStatus(final int done, final int inFlight, final int total) {
mHandler.post(() -> {
boolean finished = (done == total && mLastBlobsDigestRemain == 0);
buttonToggle.setEnabled(!finished);
progressFile.setMax(total);
progressFile.setProgress(done);
progressFile.setSecondaryProgress(done + inFlight);
if (finished) {
buttonToggle.setText(getString(R.string.pause_resume));
}
StringBuilder filesUploaded = new StringBuilder(40);
if (done < 2) {
filesUploaded.append(done).append(" file uploaded");
} else {
filesUploaded.append(done).append(" files uploaded");
}
textFileStatus.setText(filesUploaded.toString());
textBlobsRemain.setText("Files to upload: " + (total - done));
});
}
@Override
public void setByteStatus(final long done, final int inFlight, final long total) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
// setMax takes an (signed) int, but 2GB is a totally
// reasonable upload size, so use units of 1KB instead.
progressBytes.setMax((int) (total / 1024L));
progressBytes.setProgress((int) (done / 1024L));
// TODO: renable once pk-put properly sends inflight information
// progressBytes.setSecondaryProgress(progressBytes.getProgress() + inFlight / 1024);
public void setByteStatus(final long done, final int inFlight, final long total) {
mHandler.post(() -> {
// setMax takes an (signed) int, but 2GB is a totally
// reasonable upload size, so use units of 1KB instead.
progressBytes.setMax((int) (total / 1024L));
progressBytes.setProgress((int) (done / 1024L));
// TODO: renable once pk-put properly sends inflight information
// progressBytes.setSecondaryProgress(progressBytes.getProgress() + inFlight / 1024);
StringBuilder bytesUploaded = new StringBuilder(40);
if (done < 2) {
bytesUploaded.append(done).append(" byte uploaded");
} else {
bytesUploaded.append(done).append(" bytes uploaded");
}
textByteStatus.setText(bytesUploaded.toString());
StringBuilder bytesUploaded = new StringBuilder(40);
if (done < 2) {
bytesUploaded.append(done).append(" byte uploaded");
} else {
bytesUploaded.append(done).append(" bytes uploaded");
}
textByteStatus.setText(bytesUploaded.toString());
});
}
@Override
public void setUploadStatusText(final String text) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
textUploadStatus.setText(text);
}
});
public void setUploadStatusText(final String text) {
mHandler.post(() -> textUploadStatus.setText(text));
}
@Override
public void setUploadStatsText(final String text) throws RemoteException {
public void setUploadStatsText(final String text) {
// We were getting these status updates so quickly that the calls to TextView.setText
// were consuming all CPU on the main thread and it was stalling the main thread
// for seconds, sometimes even triggering device freezes. Ridiculous. So instead,
// only update this every 30 milliseconds, otherwise wait for the looper to be idle
// to update it.
mHandler.post(new Runnable() {
@Override
public void run() {
mStatusTextWant = text;
long now = System.currentTimeMillis();
if (mLastStatusUpdate < now - 30) {
mStatusTextCurrent = mStatusTextWant;
textStats.setText(mStatusTextWant);
mLastStatusUpdate = System.currentTimeMillis();
}
mHandler.post(() -> {
mStatusTextWant = text;
long now = System.currentTimeMillis();
if (mLastStatusUpdate < now - 30) {
mStatusTextCurrent = mStatusTextWant;
textStats.setText(mStatusTextWant);
mLastStatusUpdate = System.currentTimeMillis();
}
});
}
public void setUploadErrorsText(final String text) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
textErrors.setText(text);
}
});
public void setUploadErrorsText(final String text) {
mHandler.post(() -> textErrors.setText(text));
}
};
@ -321,7 +290,7 @@ public class CamliActivity extends Activity {
ProfilesActivity.show(this);
break;
case MENU_VERSION:
Toast.makeText(this, "pk-put version: " + ((UploadApplication) getApplication()).getCamputVersion(), Toast.LENGTH_LONG).show();
Toast.makeText(this, "pk-put version: " + ((UploadApplication) getApplication()).getPkPutVersion(), Toast.LENGTH_LONG).show();
break;
case MENU_UPLOAD_ALL:
Intent uploadAll = new Intent(UploadService.INTENT_UPLOAD_ALL);
@ -353,6 +322,8 @@ public class CamliActivity extends Activity {
super.onResume();
// Check for the right to read the user's files.
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
@ -388,18 +359,15 @@ public class CamliActivity extends Activity {
Log.d(TAG, "onResume; action=" + action);
if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
setIntent(new Intent(this, CamliActivity.class));
setIntent(new Intent(this, PerkeepActivity.class));
} else {
Log.d(TAG, "Normal CamliActivity viewing.");
Log.d(TAG, "Normal perkeepActivity viewing.");
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case READ_EXTERNAL_STORAGE_PERMISSION_RESPONSE: {
// If request is cancelled, the result arrays are empty.
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == READ_EXTERNAL_STORAGE_PERMISSION_RESPONSE) {// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "User authorized us to read his files.");
} else {
@ -407,8 +375,6 @@ public class CamliActivity extends Activity {
Log.d(TAG, "Permission to read files denied by user.");
System.exit(1);
}
return;
}
}
}
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package org.camlistore;
import java.io.File;
import java.nio.file.Paths;
import android.net.Uri;
import android.os.FileObserver;
@ -25,13 +26,13 @@ import android.util.Log;
import org.camlistore.IUploadService.Stub;
public class CamliFileObserver extends FileObserver {
private static final String TAG = "CamliFileObserver";
public class PerkeepFileObserver extends FileObserver {
private static final String TAG = "PerkeepFileObserver";
private final File mDirectory;
private final Stub mServiceStub;
public CamliFileObserver(IUploadService.Stub service, File directory) {
public PerkeepFileObserver(IUploadService.Stub service, File directory) {
super(directory.getAbsolutePath(), FileObserver.CLOSE_WRITE | FileObserver.MOVED_TO);
// TODO: Docs say: "The monitored file or directory must exist at this
// time, or else no events will be reported (even if it appears
@ -45,21 +46,29 @@ public class CamliFileObserver extends FileObserver {
@Override
public void onEvent(int event, String path) {
if (path == null) {
// It's null for certain directory-level events.
if (!shouldActOnEvent(path)){
return;
}
// Note from docs:
// "This method is invoked on a special FileObserver thread."
// Order in which we get events for a new camera picture:
// CREATE, OPEN, MODIFY, [OPEN, CLOSE_NOWRITE], CLOSE_WRITE
File fullFile = new File(mDirectory, path);
Log.d(TAG, "event " + event + " for " + fullFile.getAbsolutePath());
try {
mServiceStub.enqueueUpload(Uri.fromFile(fullFile));
} catch (RemoteException e) {
mServiceStub.enqueueUpload(Uri.fromFile(fullFile));
} catch (RemoteException ignored) {
}
}
private boolean shouldActOnEvent(String path) {
// It's null for certain directory-level events.
if (path == null) {
return false;
}
// Taking a photo will generate a ".pending-*" file before moving it into the proper
// path leading to double uploads sometimes ( race between enqueue and upload). We
// get around that by the heuristic of ignoring ".pending" filenames here.
if (Paths.get(path).getFileName().toString().startsWith(".pending")) {
return false;
}
// act on all other events
return true;
}
}

View File

@ -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, "");
}

View File

@ -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);

View File

@ -1,6 +1,5 @@
package org.camlistore;
import android.app.AlertDialog;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;

View File

@ -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;
}

View File

@ -35,7 +35,6 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceScreen;
@ -106,30 +105,27 @@ public class SettingsActivity extends PreferenceActivity {
maxCacheSizePref.setSummary(getString(
R.string.settings_max_cache_size_summary, mPrefs.maxCacheMb()));
OnPreferenceChangeListener onChange = new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference pref, Object newValue) {
final String key = pref.getKey();
Log.v(TAG, "preference change for: " + key);
OnPreferenceChangeListener onChange = (pref, newValue) -> {
final String key = pref.getKey();
Log.v(TAG, "preference change for: " + key);
// Note: newValue isn't yet persisted, but easiest to update the
// UI here.
String newStr = (newValue instanceof String) ? (String) newValue
: null;
if (pref == hostPref) {
updateHostSummary(newStr);
} else if (pref == passwordPref) {
updatePasswordSummary(newStr);
} else if (pref == usernamePref) {
updateUsernameSummary(newStr);
} else if (pref == maxCacheSizePref) {
if (!updateMaxCacheSizeSummary(newStr))
return false;
} else if (pref == devIPPref) {
updateDevIP(newStr);
}
return true; // yes, persist it
// Note: newValue isn't yet persisted, but easiest to update the
// UI here.
String newStr = (newValue instanceof String) ? (String) newValue
: null;
if (pref == hostPref) {
updateHostSummary(newStr);
} else if (pref == passwordPref) {
updatePasswordSummary(newStr);
} else if (pref == usernamePref) {
updateUsernameSummary(newStr);
} else if (pref == maxCacheSizePref) {
if (!updateMaxCacheSizeSummary(newStr))
return false;
} else if (pref == devIPPref) {
updateDevIP(newStr);
}
return true; // yes, persist it
};
hostPref.setOnPreferenceChangeListener(onChange);
passwordPref.setOnPreferenceChangeListener(onChange);
@ -140,7 +136,7 @@ public class SettingsActivity extends PreferenceActivity {
/**
* Receives the results from the custome QRPreference's call to the barcode scanner intent.
*
*
* This is never called if the user doesn't have a zxing barcode scanner app installed.
*/
@Override
@ -161,14 +157,14 @@ public class SettingsActivity extends PreferenceActivity {
* confirmNewSettingsDialog will set preferences based on the parameters
* in uri.
*
* It is expected the schema of uri is 'camli' and the host is 'settings'.
* It is expected the schema of uri is 'perkeep' and the host is 'settings'.
* Uri parameters expected are server, certFingerprint, username,
* autoUpload, maxCacheSize, and password
*/
private final void confirmNewSettingsDialog(final Uri uri) {
private void confirmNewSettingsDialog(final Uri uri) {
Log.v(TAG, "QR resolved to: " + uri);
if (!(uri.getScheme().equals("camli") && uri.getHost().equals("settings"))) {
Toast.makeText(this, "QR code not a camli://settings/ URL", Toast.LENGTH_LONG).show();
if (!(uri.getScheme().equals("perkeep") && uri.getHost().equals("settings"))) {
Toast.makeText(this, "QR code not a perkeep://settings/ URL", Toast.LENGTH_LONG).show();
return;
}
@ -230,21 +226,16 @@ public class SettingsActivity extends PreferenceActivity {
@Override
protected void onPause() {
super.onPause();
mSharedPrefs
.unregisterOnSharedPreferenceChangeListener(prefChangedHandler);
if (mServiceConnection != null) {
unbindService(mServiceConnection);
}
mSharedPrefs.unregisterOnSharedPreferenceChangeListener(prefChangedHandler);
unbindService(mServiceConnection);
}
@Override
protected void onResume() {
super.onResume();
updatePreferenceSummaries();
mSharedPrefs
.registerOnSharedPreferenceChangeListener(prefChangedHandler);
bindService(new Intent(this, UploadService.class), mServiceConnection,
Context.BIND_AUTO_CREATE);
mSharedPrefs.registerOnSharedPreferenceChangeListener(prefChangedHandler);
bindService(new Intent(this, UploadService.class), mServiceConnection, Context.BIND_AUTO_CREATE);
}
private void updatePreferenceSummaries() {
@ -265,8 +256,7 @@ public class SettingsActivity extends PreferenceActivity {
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
if (wifiInfo != null) {
int ip = wifiInfo.getIpAddress();
value = String.format("%d.%d.%d.", ip & 0xff, (ip >> 8) & 0xff,
(ip >> 16) & 0xff) + value;
value = String.format("%d.%d.%d.", ip & 0xff, (ip >> 8) & 0xff, (ip >> 16) & 0xff) + value;
devIPPref.setText(value);
mPrefs.setDevIP(value);
}
@ -277,8 +267,7 @@ public class SettingsActivity extends PreferenceActivity {
usernamePref.setEnabled(enabled);
passwordPref.setEnabled(enabled);
if (!enabled) {
devIPPref.setSummary("Using http://" + value
+ ":3179 user/pass \"camlistore\", \"pass3179\"");
devIPPref.setSummary("Using http://" + value + ":3179 user/pass \"perkeep\", \"pass3179\"");
} else {
devIPPref.setSummary("(Dev-server IP to override settings above)");
}

View File

@ -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) {
}
}
}
}
}

View File

@ -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);
PendingIntent pendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE|PendingIntent.FLAG_UPDATE_CURRENT);
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mNotificationChannel = new NotificationChannel(getString(R.string.channel_id),
getText(R.string.channel_name), NotificationManager.IMPORTANCE_DEFAULT);
mNotificationChannel.setDescription(getString(R.string.channel_description));
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
mNotificationManager.createNotificationChannel(mNotificationChannel);
autoUploadNotif = new Notification.Builder(this, getString(R.string.channel_id));
} else {
autoUploadNotif = new Notification.Builder(this);
}
NotificationChannel mNotificationChannel = new NotificationChannel(
getString(R.string.channel_id),
getText(R.string.channel_name),
NotificationManager.IMPORTANCE_DEFAULT);
mNotificationChannel.setDescription(getString(R.string.channel_description));
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
mNotificationManager.createNotificationChannel(mNotificationChannel);
autoUploadNotif = new Notification.Builder(this, getString(R.string.channel_id));
autoUploadNotif.setContentTitle(getText(R.string.notification_title))
.setContentText(notificationMessage())
.setSmallIcon(R.drawable.ic_stat_notify)
@ -176,16 +165,16 @@ public class UploadService extends Service {
startService(new Intent(UploadService.this, UploadService.class));
}
// This is @Override as of SDK version 5, but we're targeting 4 (Android
// 1.6)
private static final int START_STICKY = 1; // in SDK 5
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
handleCommand(intent);
// We want this service to continue running until it is explicitly
// stopped, so return sticky.
return START_STICKY;
return Service.START_STICKY;
}
private String getPkBin() {
return getApplicationInfo().nativeLibraryDir + "/libpkput.so";
}
private void handleCommand(Intent intent) {
@ -273,62 +262,55 @@ public class UploadService extends Service {
}
final Uri uri = (Uri) streamValue;
Util.runAsync(new Runnable() {
@Override
public void run() {
try {
service.enqueueUpload(uri);
} catch (RemoteException e) {
}
Util.runAsync(() -> {
try {
service.enqueueUpload(uri);
} catch (RemoteException ignored) {
}
});
}
private void handleUploadAll() {
startService(new Intent(UploadService.this, UploadService.class));
final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Camli Upload All");
final PowerManager.WakeLock wakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "PerkeepUploadService:UploadAll");
wakeLock.acquire();
Util.runAsync(new Runnable() {
@Override
public void run() {
try {
List<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) {
}
}
}

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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"

View File

@ -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" />

View File

@ -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
}

View File

@ -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

View File

@ -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