diff --git a/clients/android/Makefile b/clients/android/Makefile
index 9a9f21768..cb7942bf2 100644
--- a/clients/android/Makefile
+++ b/clients/android/Makefile
@@ -1,4 +1,6 @@
all:
+ # to run if you already have a functional android development
+ # environment, and you don't need the one in docker.
./check-environment.pl
./gradlew assembleRelease
@@ -14,6 +16,11 @@ dockerdebug:
dockerrelease:
docker run --rm -v $(GOPATH)/src/camlistore.org:/home/gopher/src/camlistore.org -v $(HOME)/.gradle:/home/gopher/.gradle -v $(HOME)/.android:/home/gopher/.android -w /home/gopher/src/camlistore.org/clients/android --name camlidroid -i -t camlistore/android go run build-in-docker.go -release=true
+# just for documentation, as make is not actually installed in the docker image.
+debug:
+ # when within the env dev (i.e. after make dockerdev)
+ ./gradlew assembleDebug
+
installdebug:
adb install -r app/build/outputs/apk/app-debug.apk
diff --git a/clients/android/app/build.gradle b/clients/android/app/build.gradle
index 63120cdb0..fe01cef9d 100644
--- a/clients/android/app/build.gradle
+++ b/clients/android/app/build.gradle
@@ -11,7 +11,7 @@ android {
defaultConfig {
applicationId "org.camlistore"
- minSdkVersion 10
+ minSdkVersion 11
targetSdkVersion 17
versionCode 1
versionName "1"
diff --git a/clients/android/app/src/main/AndroidManifest.xml b/clients/android/app/src/main/AndroidManifest.xml
index 71eb726ff..5d9f18f2b 100644
--- a/clients/android/app/src/main/AndroidManifest.xml
+++ b/clients/android/app/src/main/AndroidManifest.xml
@@ -49,6 +49,9 @@
+
+
+
diff --git a/clients/android/app/src/main/java/org/camlistore/CamliActivity.java b/clients/android/app/src/main/java/org/camlistore/CamliActivity.java
index d6216206f..1508b6a04 100644
--- a/clients/android/app/src/main/java/org/camlistore/CamliActivity.java
+++ b/clients/android/app/src/main/java/org/camlistore/CamliActivity.java
@@ -46,6 +46,7 @@ public class CamliActivity extends Activity {
private static final int MENU_STOP_DIE = 3;
private static final int MENU_UPLOAD_ALL = 4;
private static final int MENU_VERSION = 5;
+ private static final int MENU_PROFILES = 6;
private IUploadService mServiceStub = null;
private IStatusCallback mCallback = null;
@@ -251,6 +252,10 @@ public class CamliActivity extends Activity {
MenuItem stopDie = menu.add(Menu.NONE, MENU_STOP_DIE, 0, R.string.stop_die);
stopDie.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+ MenuItem profiles = menu.add(Menu.NONE, MENU_PROFILES, 0, R.string.profile);
+ // TODO(mpl): do we care about this icon? I don't even know where it actually appears.
+ profiles.setIcon(android.R.drawable.ic_menu_preferences);
+
MenuItem settings = menu.add(Menu.NONE, MENU_SETTINGS, 0, R.string.settings);
settings.setIcon(android.R.drawable.ic_menu_preferences);
@@ -275,6 +280,9 @@ public class CamliActivity extends Activity {
case MENU_SETTINGS:
SettingsActivity.show(this);
break;
+ case MENU_PROFILES:
+ ProfilesActivity.show(this);
+ break;
case MENU_VERSION:
Toast.makeText(this, "camput version: " + ((UploadApplication) getApplication()).getCamputVersion(), Toast.LENGTH_LONG).show();
break;
@@ -307,7 +315,7 @@ public class CamliActivity extends Activity {
protected void onResume() {
super.onResume();
- SharedPreferences sp = getSharedPreferences(Preferences.NAME, 0);
+ SharedPreferences sp = getSharedPreferences(Preferences.filename(this.getBaseContext()), 0);
try {
HostPort hp = new HostPort(sp.getString(Preferences.HOST, ""));
if (!hp.isValid()) {
diff --git a/clients/android/app/src/main/java/org/camlistore/Preferences.java b/clients/android/app/src/main/java/org/camlistore/Preferences.java
index 65a0ce7dd..6fa67c588 100644
--- a/clients/android/app/src/main/java/org/camlistore/Preferences.java
+++ b/clients/android/app/src/main/java/org/camlistore/Preferences.java
@@ -16,11 +16,22 @@ limitations under the License.
package org.camlistore;
+import android.content.Context;
import android.content.SharedPreferences;
public final class Preferences {
+ private static final String TAG = "Preferences";
public static final String NAME = "CamliUploader";
+ // key/value store file where we keep the profile names
+ public static final String PROFILES_FILE = "CamliUploader_profiles";
+ // key to the set of profile names
+ public static final String PROFILES = "camli.profiles";
+ // key to the currently selected profile
+ public static final String PROFILE = "camli.profile";
+ // for the preference element that lets us create a new profile name
+ public static final String NEWPROFILE = "camli.newprofile";
+
public static final String HOST = "camli.host";
// TODO(mpl): list instead of single string later? seems overkill for now.
public static final String TRUSTED_CERT = "camli.trusted_cert";
@@ -42,6 +53,18 @@ public final class Preferences {
mSP = prefs;
}
+ // filename returns the settings file name for the currently selected profile.
+ public static String filename(Context ctx) {
+ 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
+ // backwards compatible.
+ return NAME;
+ }
+ return NAME+"."+currentProfile;
+ }
+
public boolean autoRequiresPower() {
return mSP.getBoolean(AUTO_REQUIRE_POWER, false);
}
diff --git a/clients/android/app/src/main/java/org/camlistore/ProfilesActivity.java b/clients/android/app/src/main/java/org/camlistore/ProfilesActivity.java
new file mode 100644
index 000000000..e7569be62
--- /dev/null
+++ b/clients/android/app/src/main/java/org/camlistore/ProfilesActivity.java
@@ -0,0 +1,139 @@
+/*
+Copyright 2017 The Camlistore Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ */
+
+package org.camlistore;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.Bundle;
+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;
+
+public class ProfilesActivity extends PreferenceActivity {
+ private static final String TAG = "ProfilesActivity";
+ private ListPreference mProfilePref;
+ private EditTextPreference mNewProfilePref;
+ private SharedPreferences mSharedPrefs;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mSharedPrefs = getSharedPreferences(Preferences.PROFILES_FILE, 0);
+ getPreferenceManager().setSharedPreferencesName(Preferences.PROFILES_FILE);
+ // In effect, I think the default values from arrays.xml are useless since we
+ // always override them with refreshProfileRef right after.
+ addPreferencesFromResource(R.xml.profiles);
+ mProfilePref = (ListPreference) findPreference(Preferences.PROFILE);
+ 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
+ }
+ };
+ mProfilePref.setOnPreferenceChangeListener(onChange);
+ mNewProfilePref.setOnPreferenceChangeListener(onChange);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ refreshProfileRef();
+ updatePreferenceSummaries();
+ }
+
+ private void updatePreferenceSummaries() {
+ updateProfilesSummary(mProfilePref.getValue());
+ }
+
+ private void updateProfilesSummary(String value) {
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+ mProfilePref.setSummary(value);
+ }
+
+ // updateProfilesList adds value to the set of existing profiles to the
+ // key/value store, and refreshes the preference list.
+ private void updateProfilesList(String value) {
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+ String removedChars = "(%|\\?|:|\"|\\*|\\||/|\\|<|>| )";
+ value = value.replaceAll(removedChars, "");
+ if (value.isEmpty()) {
+ return;
+ }
+
+ Set profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet());
+ profiles.add(value);
+ Editor ed = mSharedPrefs.edit();
+ ed.putStringSet(Preferences.PROFILES, profiles);
+ ed.commit();
+ refreshProfileRef();
+ mProfilePref.setValue(value);
+ mProfilePref.setSummary(value);
+ Log.v(TAG, "New profile added: " + value);
+ }
+
+ // refreshProfileRef refreshes the profiles preference list with the profile
+ // values stored in the key/value file.
+ private void refreshProfileRef() {
+ Set profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet());
+ 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();
+ }
+ CharSequence[] listValues = profiles.toArray(new String[]{});
+ mProfilePref.setEntries(listValues);
+ mProfilePref.setEntryValues(listValues);
+ }
+
+ // Convenience method.
+ static void show(Context context) {
+ final Intent intent = new Intent(context, ProfilesActivity.class);
+ context.startActivity(intent);
+ }
+}
diff --git a/clients/android/app/src/main/java/org/camlistore/SettingsActivity.java b/clients/android/app/src/main/java/org/camlistore/SettingsActivity.java
index 609af323f..d296ec691 100644
--- a/clients/android/app/src/main/java/org/camlistore/SettingsActivity.java
+++ b/clients/android/app/src/main/java/org/camlistore/SettingsActivity.java
@@ -90,12 +90,10 @@ public class SettingsActivity extends PreferenceActivity {
m.put(Preferences.MAX_CACHE_MB, "maxCacheSize");
prefToParam = Collections.unmodifiableMap(m);
- getPreferenceManager().setSharedPreferencesName(Preferences.NAME);
+ getPreferenceManager().setSharedPreferencesName(Preferences.filename(this.getBaseContext()));
addPreferencesFromResource(R.xml.preferences);
hostPref = (EditTextPreference) findPreference(Preferences.HOST);
- // TODO(mpl): popup window that proposes to automatically add the cert to
- // the prefs when we fail to dial an untrusted server (and only in that case).
trustedCertPref = (EditTextPreference) findPreference(Preferences.TRUSTED_CERT);
usernamePref = (EditTextPreference) findPreference(Preferences.USERNAME);
passwordPref = (EditTextPreference) findPreference(Preferences.PASSWORD);
@@ -104,7 +102,7 @@ public class SettingsActivity extends PreferenceActivity {
maxCacheSizePref = (EditTextPreference) findPreference(Preferences.MAX_CACHE_MB);
devIPPref = (EditTextPreference) findPreference(Preferences.DEV_IP);
- mSharedPrefs = getSharedPreferences(Preferences.NAME, 0);
+ mSharedPrefs = getSharedPreferences(Preferences.filename(this.getBaseContext()), 0);
mPrefs = new Preferences(mSharedPrefs);
// Display defaults.
diff --git a/clients/android/app/src/main/java/org/camlistore/UploadService.java b/clients/android/app/src/main/java/org/camlistore/UploadService.java
index 012e55730..ca71e3b11 100644
--- a/clients/android/app/src/main/java/org/camlistore/UploadService.java
+++ b/clients/android/app/src/main/java/org/camlistore/UploadService.java
@@ -107,7 +107,7 @@ public class UploadService extends Service {
mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- mPrefs = new Preferences(getSharedPreferences(Preferences.NAME, 0));
+ mPrefs = new Preferences(getSharedPreferences(Preferences.filename(this.getBaseContext()), 0));
updateBackgroundWatchers();
}
diff --git a/clients/android/app/src/main/res/values/arrays.xml b/clients/android/app/src/main/res/values/arrays.xml
new file mode 100644
index 000000000..cc6d360ca
--- /dev/null
+++ b/clients/android/app/src/main/res/values/arrays.xml
@@ -0,0 +1,10 @@
+
+
+
+ - default
+
+
+ - default
+
+
+
diff --git a/clients/android/app/src/main/res/values/strings.xml b/clients/android/app/src/main/res/values/strings.xml
index ab462005a..810700c8c 100644
--- a/clients/android/app/src/main/res/values/strings.xml
+++ b/clients/android/app/src/main/res/values/strings.xml
@@ -5,6 +5,8 @@
Use these settings?
Camlistore server
e.g. https://foo.example.com or "example.com:3179"
+ Current profile
+ default
QR
Scan QR code from /ui/mobile.html
Self-signed cert fingerprint
@@ -31,6 +33,7 @@
Files Uploaded
Bytes Uploaded
Settings
+ Profile
Upload All
Browse
Results
diff --git a/clients/android/app/src/main/res/xml/profiles.xml b/clients/android/app/src/main/res/xml/profiles.xml
new file mode 100644
index 000000000..428f6903b
--- /dev/null
+++ b/clients/android/app/src/main/res/xml/profiles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+