/*
* 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;
import org.ccnx.android.ccnlib.CCNxServiceControl;
import org.ccnx.android.ccnlib.CCNxServiceCallback;
import org.ccnx.android.ccnlib.CCNxServiceStatus.SERVICE_STATUS;
import org.ccnx.android.ccnlib.CcndWrapper.CCND_OPTIONS;
import org.ccnx.android.ccnlib.RepoWrapper.CCNR_OPTIONS;
import org.ccnx.android.ccnlib.RepoWrapper.REPO_OPTIONS;
import org.ccnx.android.ccnlib.RepoWrapper.CCNS_OPTIONS;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.IntentFilter;
import android.content.Intent;
import android.content.BroadcastReceiver;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Build;
import android.os.AsyncTask;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.view.MenuItem;
import android.view.Menu;
import android.view.MenuInflater;
import android.widget.Button;
import android.widget.TextView;
import android.widget.EditText;
import android.widget.Toast;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Spinner;
import android.net.Uri;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
/**
* Android UI for controlling CCNx services.
*/
public final class Controller extends Activity implements OnClickListener {
public final static String TAG = "CCNx Service Controller";
public static final String CCNX_WS_URL = "http://127.0.0.1:9695";
private Button mAllBtn;
private ProgressDialog pd;
private Context _ctx;
private TextView tvCcndStatus;
private TextView tvRepoStatus;
private TextView deviceIPAddress = null;
private CCNxServiceControl control;
private String mReleaseVersion = "Unknown";
private BroadcastReceiver mReceiver;
// Create a handler to receive status updates
private final Handler _handler = new Handler() {
public void handleMessage(Message msg){
SERVICE_STATUS st = SERVICE_STATUS.fromOrdinal(msg.what);
Log.d(TAG,"Received new status from CCNx Services: " + st.name());
// This is very very lazy. Instead of checking what we got, we'll just
// update the state and let that get our new status
// Considering above comment, we should decide whether this is overly complex and implement a state machine
// that can be rigorously tested for state transitions, and is adhered to in the UI status notifications
if ((st == SERVICE_STATUS.START_ALL_DONE) || (st == SERVICE_STATUS.STOP_ALL_DONE)) {
mAllBtn.setText(R.string.allStartButton);
mAllBtn.setEnabled(true);
}
if (st == SERVICE_STATUS.START_ALL_ERROR) {
Toast.makeText(_ctx, "Unable to Start Services. Reason:" + control.getErrorMessage(), 20).show();
mAllBtn.setText(R.string.allStartButton_Error);
}
// Update the UI after we receive a notification, otherwise we won't capture all state changes
updateState();
}
};
CCNxServiceCallback cb = new CCNxServiceCallback() {
public void newCCNxStatus(SERVICE_STATUS st) {
_handler.sendEmptyMessage(st.ordinal());
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.controllermain);
Log.d(TAG,"Creating Service Controller");
_ctx = this.getApplicationContext();
init();
initUI();
updateState();
}
@Override
protected void onPause() {
super.onPause();
// We should be saving out the state here for the UI so we don't lose user settings
this.unregisterReceiver(mReceiver);
}
@Override
public void onDestroy() {
control.disconnect();
super.onDestroy();
}
@Override
public void onResume() {
super.onResume();
IntentFilter intentfilter = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE");
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Received android.net.conn.CONNECTIVITY_CHANGE");
// updateIPAddress();
new GetIPAddrAsyncTask().execute();
}
};
this.registerReceiver(mReceiver, intentfilter);
//
// Update on resume, as frequently, in old Android esp, WIFI gets
// shut off and may lose the address it had
//
// updateIPAddress();
new GetIPAddrAsyncTask().execute();
// We should updateState on resuming, in case Service state has changed
updateState();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.servicemenu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.reset:
// Need to figure out if this is always safe to call even when nothing is running
control.stopAll();
control.clearErrorMessage();
Toast.makeText(this, "Reset CCNxServiceStatus complete, new status is: {ccnd: " + control.getCcndStatus().name() +
", repo: " + control.getRepoStatus().name() + "}", 10).show();
return true;
case R.id.ccndstatus:
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(CCNX_WS_URL));
startActivity(intent);
return true;
case R.id.about:
setContentView(R.layout.aboutview);
TextView aboutdata = (TextView) findViewById(R.id.about_text);
aboutdata.setText(mReleaseVersion + "\n" + aboutdata.getText());
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void init(){
Log.d(TAG, "init()");
control = new CCNxServiceControl(this);
control.registerCallback(cb);
control.connect();
try {
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
mReleaseVersion = TAG + " " + pInfo.versionName;
} catch(NameNotFoundException e) {
Log.e(TAG, "Could not find package name. Reason: " + e.getMessage());
}
}
public void onClick(View v) {
switch( v.getId() ) {
case R.id.allStartButton:
allButton();
break;
default:
Log.e(TAG, "Clicked unknown view");
}
}
private void updateState(){
if(control.isAllRunning()){
mAllBtn.setText(R.string.allStopButton);
mAllBtn.setEnabled(true);
Log.d(TAG, "Repo and CCND are running, enable button");
} else if (((control.getCcndStatus() == SERVICE_STATUS.SERVICE_OFF) && (control.getRepoStatus() == SERVICE_STATUS.SERVICE_OFF)) ||
((control.getCcndStatus() == SERVICE_STATUS.SERVICE_FINISHED) && (control.getRepoStatus() == SERVICE_STATUS.SERVICE_FINISHED))
) {
Log.d(TAG, "Repo and CCND are both finished/off");
} else {
// We've potentially got to wait longer, or we've got problems
// If we've got problems, report it via notifcation to taskbar
if ((control.getCcndStatus() == SERVICE_STATUS.SERVICE_ERROR) || (control.getRepoStatus() == SERVICE_STATUS.SERVICE_ERROR)) {
Log.e(TAG, "Error in CCNxServiceStatus. Need to clear error and reset state");
// Toast it for now
Toast.makeText(this, "Error in CCNxServiceStatus. Need to clear error and reset state", 20).show();
}
}
tvCcndStatus.setText(control.getCcndStatus().name());
tvRepoStatus.setText(control.getRepoStatus().name());
}
/**
* Start all services in the background
*/
private void allButton(){
// Always disable the button after a click until we
// reach a stable state, or hit error condition
mAllBtn.setEnabled(false);
mAllBtn.setText(R.string.allStartButton_Processing);
Log.d(TAG, "Disabling All Button");
if(control.isAllRunning()){
// Everything is ready, we must stop
control.stopAll();
} else { /* Note, this doesn't take into account partially running state */
// Not all running... attempt to start them
// but first, get the user settings
// Consider these to be our defaults
// We don't really check validity of the data in terms of constraints
// so we should shore this up to be more robust
final EditText ccnrDir = (EditText) findViewById(R.id.key_ccnr_directory);
String val = ccnrDir.getText().toString();
if (isValid(val)) {
control.setCcnrOption(CCNR_OPTIONS.CCNR_DIRECTORY, val);
} else {
// Toast it, and return (so the user fixes the bum field)
Toast.makeText(this, "CCNR_DIRECTORY field is not valid. Please set and then start.", 10).show();
return;
}
final EditText ccnrGlobalPrefix= (EditText) findViewById(R.id.key_ccnr_global_prefix);
val = ccnrGlobalPrefix.getText().toString();
if (isValid(val)) {
control.setCcnrOption(CCNR_OPTIONS.CCNR_GLOBAL_PREFIX, val);
} else {
// Toast it, and return (so the user fixes the bum field)
Toast.makeText(this, "CCNR_GLOBAL_PREFIX field is not valid. Please set and then start.", 10).show();
return;
}
final Spinner ccnrDebugSpinner = (Spinner) findViewById(R.id.key_ccnr_debug);
val = ccnrDebugSpinner.getSelectedItem().toString();
if (isValid(val)) {
control.setCcnrOption(CCNR_OPTIONS.CCNR_DEBUG, val);
} else {
// Toast it, and return (so the user fixes the bum field)
// XXX I Don't think this will ever happen
Toast.makeText(this, "CCNR_DEBUG field is not valid. Please set and then start.", 10).show();
return;
}
final Spinner ccnsDebugSpinner = (Spinner) findViewById(R.id.key_ccns_debug);
val = ccnsDebugSpinner.getSelectedItem().toString();
if (isValid(val)) {
control.setSyncOption(CCNS_OPTIONS.CCNS_DEBUG, val);
} else {
// Toast it, and return (so the user fixes the bum field)
// XXX I Don't think this will ever happen
Toast.makeText(this, "CCNS_DEBUG field is not valid. Please set and then start.", 10).show();
return;
}
control.startAllInBackground();
}
// updateState();
}
private void initUI() {
mAllBtn = (Button)findViewById(R.id.allStartButton);
mAllBtn.setOnClickListener(this);
tvCcndStatus = (TextView)findViewById(R.id.tvCcndStatus);
tvRepoStatus = (TextView)findViewById(R.id.tvRepoStatus);
deviceIPAddress = (TextView)findViewById(R.id.deviceIPAddress);
new GetIPAddrAsyncTask().execute();
//
// Grab the LinearLayout in the 0th child element
//
ViewGroup layout = (ViewGroup) findViewById(R.id.scrollcontainer); // .getChildAt(0);
ViewGroup layoutchild = (ViewGroup) layout.getChildAt(0);
if (Build.VERSION.SDK_INT >= 0x0000000e) { // ICS or greater, requires at least SDK 14 to compile
final android.widget.Switch swbtn = new android.widget.Switch(this);
swbtn.setOnCheckedChangeListener(new ToggleOptionChangeListener(CCNS_OPTIONS.CCNS_ENABLE));
swbtn.setChecked(true);
layoutchild.addView(swbtn);
} else { // Fall back to pre-ICS widget
android.widget.ToggleButton tbtn = new android.widget.ToggleButton(this);
tbtn.setOnCheckedChangeListener(new ToggleOptionChangeListener(CCNS_OPTIONS.CCNS_ENABLE));
tbtn.setChecked(true);
layoutchild.addView(tbtn);
}
}
private class ToggleOptionChangeListener implements CompoundButton.OnCheckedChangeListener {
//
// In principle, it would be nice to have a generalized listener for toggle poperties, rather than writing
// one for each property. Due to the fact our OPTIONS are not Strings or int primitives, it gets a little
// messy to make this in a general way. We should consider changing the OPTIONS to be int primitives while
// the impact of such changes will be minimal rather than maintaining this as a set of enums, which has been a
// pain
//
private CCNS_OPTIONS mCcns_option = null;
private CCNR_OPTIONS mCcnr_option = null;
private REPO_OPTIONS mRepo_option = null;
private CCND_OPTIONS mCcnd_option = null;
public ToggleOptionChangeListener(CCNS_OPTIONS option) {
mCcns_option = option;
}
public ToggleOptionChangeListener(CCNR_OPTIONS option) {
mCcnr_option = option;
}
public ToggleOptionChangeListener(REPO_OPTIONS option) {
mRepo_option = option;
}
public ToggleOptionChangeListener(CCND_OPTIONS option) {
mCcnd_option = option;
}
public void onCheckedChanged (CompoundButton buttonView, boolean isChecked) {
String val = isChecked ? "1" : "0";
if (control == null) {
Log.e(TAG, "control is null, failing");
return;
}
if (mCcns_option != null) {
control.setSyncOption(mCcns_option, val);
} else if (mCcnr_option != null) {
control.setCcnrOption(mCcnr_option, val);
} else if (mRepo_option != null) {
control.setRepoOption(mRepo_option, val);
} else if (mCcnd_option != null) {
control.setCcndOption(mCcnd_option, val);
} else {
Log.e(TAG, "null OPTION specificed for ToggleOptionChangeListener, ignoring.");
}
}
}
private String getIPAddress() {
try {
for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements();) {
NetworkInterface nic = e.nextElement();
Log.d(TAG,"---------------------------------");
Log.d(TAG,"NIC: " + nic.toString());
Log.d(TAG,"---------------------------------");
for (Enumeration<InetAddress> enumIpAddr = nic.getInetAddresses(); enumIpAddr.hasMoreElements();) {
InetAddress addr = enumIpAddr.nextElement();
if(addr != null)
{
Log.d(TAG, " HostName: " + addr.getHostName());
Log.d(TAG, " Class: " + addr.getClass().getSimpleName());
Log.d(TAG, " IP: " + addr.getHostAddress());
Log.d(TAG, " CanonicalHost: " + addr.getCanonicalHostName());
Log.d(TAG, " Is SiteLocal?: " + addr.isSiteLocalAddress());
}
if (!addr.isLoopbackAddress()) {
return addr.getHostAddress().toString();
}
}
}
} catch (SocketException ex) {
Toast.makeText(this, "Error obtaining IP Address. Reason: " + ex.getMessage(), 10).show();
// If we can't get our IP, we got problems
// Report it
Log.e(TAG, "Error obtaining IP Address. Reason: " + ex.getMessage());
}
return null;
}
private boolean isValid(String val) {
// Normally we'd do real field validation to make sure input matches type of input
return (!((val == null) || (val.length() == 0)));
}
private void updateIPAddress(String ipaddr) {
if (ipaddr != null) {
deviceIPAddress.setText(ipaddr);
} else {
deviceIPAddress.setText("Unable to determine IP Address");
}
}
public void aboutviewButtonListener (View view) {
// Called with user clicks OK, return to main view
setContentView(R.layout.controllermain);
initUI();
updateState();
}
private class GetIPAddrAsyncTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
String result = getIPAddress();
Log.d(TAG, "GetIPAddrAsyncTask result = " + result);
return getIPAddress();
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
Controller.this.updateIPAddress(result);
}
}
}