From 8a992c465225c6e0f1520e0eceaf88ad910f81e0 Mon Sep 17 00:00:00 2001 From: mpl Date: Thu, 1 Jun 2017 18:04:39 +0200 Subject: [PATCH] clients/android: add multiple profiles feature When one has/uses several Camlistore servers, it is tedious to change all the configuration fields (host name, user, pass, certificate) whenever one wants to upload to a different server from the one that is currently configured. Therefore, this change adds a new entry to the main menu that allows to create new profiles, and to switch between profiles. Each profile has its own configuration file, which enables to switch between servers effortlessly once they're all configured. https://storage.googleapis.com/camlistore-screenshots/Screenshot_20170606-151931.png https://storage.googleapis.com/camlistore-screenshots/Screenshot_20170606-152007.png https://storage.googleapis.com/camlistore-screenshots/Screenshot_20170606-152026.png https://storage.googleapis.com/camlistore-screenshots/Screenshot_20170606-152041.png Mininum SDK API increased to 11 because of SharedPreferences's getStringSet. Change-Id: I52e2de9e67e84188b1a4b16e046a0d47a35efc62 --- clients/android/Makefile | 7 + clients/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 3 + .../java/org/camlistore/CamliActivity.java | 10 +- .../main/java/org/camlistore/Preferences.java | 23 +++ .../java/org/camlistore/ProfilesActivity.java | 139 ++++++++++++++++++ .../java/org/camlistore/SettingsActivity.java | 6 +- .../java/org/camlistore/UploadService.java | 2 +- .../app/src/main/res/values/arrays.xml | 10 ++ .../app/src/main/res/values/strings.xml | 3 + .../android/app/src/main/res/xml/profiles.xml | 20 +++ 11 files changed, 218 insertions(+), 7 deletions(-) create mode 100644 clients/android/app/src/main/java/org/camlistore/ProfilesActivity.java create mode 100644 clients/android/app/src/main/res/values/arrays.xml create mode 100644 clients/android/app/src/main/res/xml/profiles.xml 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 @@ + + + + + + + +