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