/* * CCNx Android Services * * Copyright (C) 2010-2012 Palo Alto Research Center, Inc. * * This work is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License version 2 as published by the * Free Software Foundation. * This work is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ package org.ccnx.android.services.repo; import java.io.ByteArrayInputStream; import java.io.File; import java.util.Properties; import java.util.logging.Level; import java.util.regex.Pattern; import java.util.Map.Entry; import java.util.HashMap; import org.ccnx.android.ccnlib.CCNxServiceStatus.SERVICE_STATUS; import org.ccnx.android.ccnlib.RepoWrapper.REPO_OPTIONS; import org.ccnx.android.ccnlib.RepoWrapper.CCNR_OPTIONS; import org.ccnx.android.ccnlib.RepoWrapper.CCNS_OPTIONS; import org.ccnx.android.services.CCNxService; import org.ccnx.ccn.config.UserConfiguration; import org.ccnx.ccn.impl.repo.LogStructRepoStore; import org.ccnx.ccn.impl.repo.RepositoryServer; import org.ccnx.ccn.impl.repo.RepositoryStore; import android.content.Intent; import android.content.SharedPreferences; import android.os.Environment; import android.util.Log; /** * CCNxService specialization for the Repository. */ public final class RepoService extends CCNxService { public static final String CLASS_TAG = "CCNxRepoService"; private RepositoryServer _server=null; private RepositoryStore _repo=null; public final static String DEFAULT_REPO_DEBUG = "WARNING"; public final static String DEFAULT_REPO_LOCAL_NAME = "/local"; public final static String DEFAULT_REPO_GLOBAL_NAME = "/ccnx/repos"; public final static String DEFAULT_REPO_DIR = "/ccnx/repo"; public final static String DEFAULT_REPO_NAMESPACE = "/"; public final static String DEFAULT_SYNC_ENABLE = "1"; public final static String DEFAULT_SYNC_DEBUG = "WARNING"; public final static String DEFAULT_REPO_PROTO = "unix"; private String repo_dir = null; private String repo_debug = null; private String repo_local_name = null; private String repo_global_name = null; private String repo_namespace = null; /* We should version the impl * However we only provide versions bundled * with the CCNx release, currently just v1 and v2. * Does it make sense to use semver for our repo? * Presumably we'll keep legacy versions around for backwards compatibility. * But in addition to api versions, we'll potentially have pluggable repos, so * will need to be able to load the version of repo at runtime. So may be that * will use reflection to load the repo instead of an ugly if-else/switch statement that * we have at the moment. */ private String repo_version = "2.0.0"; // XXX Make this configurable via Android Menu // used for startup & shutdown protected Object _lock = new Object(); public RepoService(){ TAG=CLASS_TAG; } protected void onStartService(Intent intent) { Log.d(TAG, "onStartService - Starting"); boolean isPrefSet = false; // Get all the CCND options from the intent // If no option is found on intent, look in System properties // If no system property is set, fallback to preferences // And while settings OPTIONS, set preferences // We will only attempt a recovery if we are running REPO 2.0.0 SharedPreferences.Editor prefsEditor = mCCNxServicePrefs.edit(); if (intent != null) { if (Pattern.matches("1\\.0\\.0", repo_version)) { try { Properties props = new Properties(); byte [] opts = intent.getByteArrayExtra("vm_options"); if( null != opts ) { ByteArrayInputStream bais = new ByteArrayInputStream(opts); props.loadFromXML(bais); System.getProperties().putAll(props); } } catch(Exception e) { e.printStackTrace(); } Log.d(TAG, "CCN_DIR = " + UserConfiguration.userConfigurationDirectory()); Log.d(TAG, "DEF_ALIS = " + UserConfiguration.defaultKeyAlias()); Log.d(TAG, "KEY_DIR = " + UserConfiguration.keyRepositoryDirectory()); Log.d(TAG, "KEY_FILE = " + UserConfiguration.keystoreFileName()); Log.d(TAG, "USER_NAME = " + UserConfiguration.userName()); if(intent.hasExtra(REPO_OPTIONS.REPO_DIRECTORY.name())){ repo_dir = intent.getStringExtra(REPO_OPTIONS.REPO_DIRECTORY.name()); } if(intent.hasExtra(REPO_OPTIONS.REPO_DEBUG.name())){ repo_debug = intent.getStringExtra(REPO_OPTIONS.REPO_DEBUG.name()); } else { repo_debug = DEFAULT_REPO_DEBUG; } if(intent.hasExtra(REPO_OPTIONS.REPO_LOCAL.name())){ repo_local_name = intent.getStringExtra(REPO_OPTIONS.REPO_LOCAL.name()); } else { repo_local_name = DEFAULT_REPO_LOCAL_NAME; } if(intent.hasExtra(REPO_OPTIONS.REPO_GLOBAL.name())){ repo_global_name = intent.getStringExtra(REPO_OPTIONS.REPO_GLOBAL.name()); } else { repo_global_name = DEFAULT_REPO_GLOBAL_NAME; } if(intent.hasExtra(REPO_OPTIONS.REPO_NAMESPACE.name())){ repo_namespace = intent.getStringExtra(REPO_OPTIONS.REPO_NAMESPACE.name()); } else { repo_namespace = DEFAULT_REPO_NAMESPACE; } } else if (Pattern.matches("2\\.0\\.0", repo_version)) { for( CCNR_OPTIONS opt : CCNR_OPTIONS.values() ) { if(!intent.hasExtra(opt.name())){ continue; } String s = intent.getStringExtra( opt.name() ); if( null == s ) s = System.getProperty(opt.name()); Log.d(TAG,"setting option " + opt.name() + " = " + s); if( s != null ) { options.put(opt.name(), s); isPrefSet = true; prefsEditor.putString(opt.name(), s); } } for( CCNS_OPTIONS opt : CCNS_OPTIONS.values() ) { if(!intent.hasExtra(opt.name())){ continue; } String s = intent.getStringExtra( opt.name() ); if( null == s ) s = System.getProperty(opt.name()); Log.d(TAG,"setting option " + opt.name() + " = " + s); if( s != null ) { options.put(opt.name(), s); isPrefSet = true; prefsEditor.putString(opt.name(), s); } } if (isPrefSet) { prefsEditor.commit(); } } else { Log.d(TAG,"Unknown Repo version " + repo_version + " specified, failed to start Repo."); setStatus(SERVICE_STATUS.SERVICE_ERROR); } } else { // We must load options from prefs options = new HashMap<String, String>((HashMap<String, String>)mCCNxServicePrefs.getAll()); } Load(); } @Override protected void runService() { setStatus(SERVICE_STATUS.SERVICE_INITIALIZING); if (Pattern.matches("1\\.0\\.0", repo_version)) { try { repo_dir = createRepoDir(repo_dir); Log.d(TAG,"Using repo directory " + repo_dir); Log.d(TAG,"Using repo debug " + repo_debug); // Set the log level for the Repo Log.d(TAG, "Setting CCNx Logging FAC_ALL to " + repo_debug); org.ccnx.ccn.impl.support.Log.setLevel(org.ccnx.ccn.impl.support.Log.FAC_ALL, Level.parse(repo_debug)); synchronized(_lock) { if( null == _repo ) { _repo = new LogStructRepoStore(); int count = 0; while( count < 3 ) { try { count++; _repo.initialize(repo_dir, null, repo_local_name, repo_global_name, repo_namespace, null); break; } catch(Exception e) { if( count >= 3 ) throw e; try { Log.d(TAG,"Experiencing problems starting REPO, try again..."); Thread.sleep(1000); } catch(InterruptedException ie) { } } } Log.d(TAG,"Repo version 1 starting using Java-based repo"); _server = new RepositoryServer(_repo); setStatus(SERVICE_STATUS.SERVICE_RUNNING); _server.start(); } } } catch(Exception e) { e.printStackTrace(); Log.d(TAG, "Exception while invoking runService(). Reason: " + e.getMessage()); setStatus(SERVICE_STATUS.SERVICE_ERROR); } finally { thd = null; } } else if (Pattern.matches("2\\.0\\.0", repo_version)) { Log.d(TAG,"Repo version 2 starting using native C-based repo optimized for ARMv7"); /* Only set defaults when the CCNR/SYNC defaults are not appropriate for Android */ if(options.get(CCNR_OPTIONS.CCNR_DEBUG.name()) == null) { options.put(CCNR_OPTIONS.CCNR_DEBUG.name(), DEFAULT_REPO_DEBUG); } else { Log.d(TAG,CCNR_OPTIONS.CCNR_DEBUG.name() + " = " + options.get(CCNR_OPTIONS.CCNR_DEBUG.name())); } /* Make sure this directory is on the external storage */ if(options.get(CCNR_OPTIONS.CCNR_DIRECTORY.name()) == null){ repo_dir = DEFAULT_REPO_DIR; options.put(CCNR_OPTIONS.CCNR_DIRECTORY.name(), repo_dir); } else { repo_dir = options.get(CCNR_OPTIONS.CCNR_DIRECTORY.name()); if (!repo_dir.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) { repo_dir = Environment.getExternalStorageDirectory().getAbsolutePath() + repo_dir; options.put(CCNR_OPTIONS.CCNR_DIRECTORY.name(), repo_dir); Log.d(TAG, "CCNR_DIRECTORY remapped to contain external storage path = " + repo_dir); } Log.d(TAG,CCNR_OPTIONS.CCNR_DIRECTORY.name() + " = " + repo_dir); } if(options.get(CCNR_OPTIONS.CCNR_GLOBAL_PREFIX.name()) == null) { options.put(CCNR_OPTIONS.CCNR_GLOBAL_PREFIX.name(), DEFAULT_REPO_GLOBAL_NAME); } else { Log.d(TAG,CCNR_OPTIONS.CCNR_GLOBAL_PREFIX.name() + " = " + options.get(CCNR_OPTIONS.CCNR_GLOBAL_PREFIX.name())); } /* Take CCNR_BTREE_* defaults */ /* Take CCNR_CONTENT_CACHE defaults */ /* Take CCNR_MIN_SEND_BUFSIZE */ /* If we decide to split the CCND and CCNR services into separate APKs * We'll need to toggle this to use tcp */ if(options.get(CCNR_OPTIONS.CCNR_PROTO.name()) == null) { options.put(CCNR_OPTIONS.CCNR_PROTO.name(), DEFAULT_REPO_PROTO); } else { Log.d(TAG,CCNR_OPTIONS.CCNR_PROTO.name() + " = " + CCNR_OPTIONS.CCNR_PROTO.name()); } /* Take CCNR_LISTEN_ON defaults */ /* Take CCNR_STATUS_PORT defaults */ /* Take all CCNS_* defaults */ /* Don't bother with undocumented CCNS_* variables */ if ((repo_dir = createRepoDir(repo_dir)) == null) { // // If we can't create the directory // reasons: no perms, external storage unavailable // then we cannot proceed Log.e(TAG,"Repo version 2 unable to start because cannot create repo_dir"); setStatus(SERVICE_STATUS.SERVICE_ERROR); return; } try { for( Entry<String,String> entry : options.entrySet() ) { Log.d(TAG, "options key setenv: " + entry.getKey()); ccnrSetenv(entry.getKey(), entry.getValue(), 1); } if (ccnrCreate(repo_version) == 0) { setStatus(SERVICE_STATUS.SERVICE_RUNNING); try { ccnrRun(); } finally { ccnrDestroy(); } } else { // If we have problems initially creating the CCNR handle, we should shutdown with error Log.d(TAG,"ccnrCreate failure, failed to start Repo."); setStatus(SERVICE_STATUS.SERVICE_ERROR); } } catch(Exception e) { e.printStackTrace(); Log.d(TAG, "Exception caught while starting up/shutting down. Reason: " + e.getMessage()); setStatus(SERVICE_STATUS.SERVICE_ERROR); // returning will end the thread } serviceStopped(); } else { Log.d(TAG,"Unknown Repo version " + repo_version + " specified, failed to start Repo."); setStatus(SERVICE_STATUS.SERVICE_ERROR); } } protected void stopService(){ Log.i(TAG,"stopService() called"); setStatus(SERVICE_STATUS.SERVICE_TEARING_DOWN); if (Pattern.matches("1\\.0\\.0", repo_version)) { if( _server != null ) { Log.i(TAG,"calling _server.shutDown()"); _server.shutDown(); _server = null; } } else if (Pattern.matches("2\\.0\\.0", repo_version)) { setStatus(SERVICE_STATUS.SERVICE_TEARING_DOWN); ccnrKill(); } else { Log.d(TAG,"Unknown Repo version " + repo_version + " specified, failed to stop Repo."); setStatus(SERVICE_STATUS.SERVICE_ERROR); } setStatus(SERVICE_STATUS.SERVICE_FINISHED); // XXX Is it really ok to assume we've stopped when we might get errors? } private String createRepoDir(String repodir) { File f; File external_dir = Environment.getExternalStorageDirectory(); if(repodir != null) { // Check if repodir contains the external storage path if (!repodir.startsWith(external_dir.getAbsolutePath())) { repodir = external_dir.getAbsolutePath() + repodir; } f = new File(repodir); if (f.mkdirs()) { Log.d(TAG,"Created repodir = " + repodir); } else { Log.d(TAG,"Unable to create repodir = " + repodir + ", already exists"); } } else { // repo_dir is null, lets get a directory from the android system // in external storage. f = new File(external_dir.getAbsolutePath() + DEFAULT_REPO_DIR); if (f.mkdirs()) { Log.d(TAG,"Created default repodir = " + external_dir.getAbsolutePath() + DEFAULT_REPO_DIR); } else { Log.d(TAG,"Unable to create default repodir = " + external_dir.getAbsolutePath() + DEFAULT_REPO_DIR + ", already exists"); } repodir = f.getAbsolutePath(); } return repodir; } private boolean deleteRepoDir() { File f; if(repo_dir != null) { f = new File(repo_dir); try { return (f.delete()); } catch(SecurityException se) { Log.e(TAG, "deleteRepoDir SecurityException: " + se.getMessage()); return false; } } else { return false; } } protected native int ccnrCreate(String version); protected native int ccnrRun(); protected native int ccnrDestroy(); protected native int ccnrKill(); protected native void ccnrSetenv(String key, String value, int overwrite); static { // // load library try { System.loadLibrary("controller"); Log.e(CLASS_TAG, "loaded native library: controller"); } catch(UnsatisfiedLinkError ule) { Log.e(CLASS_TAG, "Unable to load native library: controller"); } } }