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
This commit is contained in:
mpl 2017-06-01 18:04:39 +02:00
parent 68cd59797a
commit 8a992c4652
11 changed files with 218 additions and 7 deletions

View File

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

View File

@ -11,7 +11,7 @@ android {
defaultConfig {
applicationId "org.camlistore"
minSdkVersion 10
minSdkVersion 11
targetSdkVersion 17
versionCode 1
versionName "1"

View File

@ -49,6 +49,9 @@
<activity android:name=".SettingsActivity">
</activity>
<activity android:name=".ProfilesActivity">
</activity>
<receiver android:name=".OnBootReceiver">
<intent-filter>

View File

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

View File

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

View File

@ -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<String> profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet<String>());
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<String> profiles = mSharedPrefs.getStringSet(Preferences.PROFILES, new HashSet<String>());
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);
}
}

View File

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

View File

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

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="profiles_entries">
<item>default</item>
</string-array>
<string-array name="profiles_entry_values">
<item>default</item>
</string-array>
</resources>

View File

@ -5,6 +5,8 @@
<string name="settings_confirmation_dialog_title">Use these settings?</string>
<string name="settings_host_title">Camlistore server</string>
<string name="settings_host_summary">e.g. https://foo.example.com or &quot;example.com:3179&quot;</string>
<string name="profiles_title">Current profile</string>
<string name="profiles_summary">default</string>
<string name="settings_qr_title">QR</string>
<string name="settings_qr_summary">Scan QR code from /ui/mobile.html</string>
<string name="settings_trusted_cert_title">Self-signed cert fingerprint</string>
@ -31,6 +33,7 @@
<string name="files_uploaded">Files Uploaded</string>
<string name="bytes_uploaded">Bytes Uploaded</string>
<string name="settings">Settings</string>
<string name="profile">Profile</string>
<string name="upload_all">Upload All</string>
<string name="browse">Browse</string>
<string name="results">Results</string>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:key="profilescreen" >
<ListPreference
android:key="camli.profile"
android:persistent="true"
android:summary="@string/profiles_summary"
android:title="@string/profiles_title"
android:entries="@array/profiles_entries"
android:entryValues="@array/profiles_entry_values"
android:defaultValue="default"/>
<EditTextPreference
android:key="camli.newprofile"
android:persistent="false"
android:summary="Create a new profile"
android:title="New Profile" />
</PreferenceScreen>