mirror of https://github.com/perkeep/perkeep.git
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:
parent
5af672029e
commit
1ca9f45c66
|
@ -2,7 +2,10 @@ build
|
|||
gen
|
||||
bin
|
||||
local.properties
|
||||
keystore.properties
|
||||
test/local.properties
|
||||
test/build
|
||||
test/gen
|
||||
test/bin
|
||||
app/libs
|
||||
app/release
|
|
@ -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)
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -43,7 +43,7 @@ interface IUploadService {
|
|||
|
||||
// Stop stop uploads, clear queues.
|
||||
void stopEverything();
|
||||
|
||||
|
||||
// For the SettingsActivity
|
||||
void setBackgroundWatchersEnabled(boolean enabled);
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
pk-put.arm
|
||||
pk-put-version.txt
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
Put pk-put.arm here. It's in .gitignore because it's 6.5 MB.
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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, "");
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package org.camlistore;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)");
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue