diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index a3d78409..0df88aac 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ android:theme="@style/Theme.AppCompat" > @@ -27,12 +27,20 @@ + + + + + android:value=".gui.MainActivity" /> diff --git a/src/main/java/com/nutomic/syncthingandroid/NodeAdapter.java b/src/main/java/com/nutomic/syncthingandroid/NodeAdapter.java new file mode 100644 index 00000000..2c5e64f8 --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/NodeAdapter.java @@ -0,0 +1,45 @@ +package com.nutomic.syncthingandroid; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import com.nutomic.syncthingandroid.syncthing.RestApi; + +import java.util.List; + +/** + * Generates item views for node items. + */ +public class NodeAdapter extends ArrayAdapter { + + public NodeAdapter(Context context) { + super(context, R.layout.node_list_item); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.node_list_item, parent, false); + } + TextView name = (TextView) convertView.findViewById(R.id.name); + name.setText(getItem(position).Name); + return convertView; + } + + + /** + * Replacement for addAll, which is not implemented on lower API levels. + */ + public void add(List nodes) { + for (RestApi.Node n : nodes) { + add(n); + } + } + +} diff --git a/src/main/java/com/nutomic/syncthingandroid/RepositoryAdapter.java b/src/main/java/com/nutomic/syncthingandroid/RepositoryAdapter.java new file mode 100644 index 00000000..d46c820b --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/RepositoryAdapter.java @@ -0,0 +1,45 @@ +package com.nutomic.syncthingandroid; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import com.nutomic.syncthingandroid.syncthing.RestApi; + +import java.util.List; + +/** + * Generates item views for repository items. + */ +public class RepositoryAdapter extends ArrayAdapter { + + public RepositoryAdapter(Context context) { + super(context, R.layout.node_list_item); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.repository_list_item, parent, false); + } + TextView id = (TextView) convertView.findViewById(R.id.id); + id.setText(getItem(position).ID); + return convertView; + } + + + /** + * Replacement for addAll, which is not implemented on lower API levels. + */ + public void add(List nodes) { + for (RestApi.Repository r : nodes) { + add(r); + } + } + +} diff --git a/src/main/java/com/nutomic/syncthingandroid/gui/LoadingListFragment.java b/src/main/java/com/nutomic/syncthingandroid/gui/LoadingListFragment.java new file mode 100644 index 00000000..58255f30 --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/gui/LoadingListFragment.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * 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 com.nutomic.syncthingandroid.gui; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.ListFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; +import android.widget.TextView; + +import com.nutomic.syncthingandroid.R; +import com.nutomic.syncthingandroid.syncthing.RestApi; +import com.nutomic.syncthingandroid.syncthing.SyncthingService; + +/** + * {@link android.support.v4.app.ListFragment} that shows a configurable loading text. + */ +public abstract class LoadingListFragment extends Fragment implements RestApi.OnApiAvailableListener { + + private boolean mInitialized = false; + + private ListFragment mListFragment; + + private View mListFragmentHolder; + + private View mLoading; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + + + if (savedInstanceState != null) { + mListFragment = (ListFragment) getChildFragmentManager() + .getFragment(savedInstanceState, ListFragment.class.getName()); + } + else { + mListFragment = new ListFragment(); + } + getChildFragmentManager() + .beginTransaction() + .add(R.id.list_fragment, mListFragment) + .commit(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.loading_list_fragment, container, false); + mListFragmentHolder = view.findViewById(R.id.list_fragment); + mLoading = view.findViewById(R.id.loading); + TextView loadingTextView = (TextView) view.findViewById(R.id.loading_text); + + if (SyncthingService.isFirstStart(getActivity())) { + loadingTextView.setText(getString(R.string.web_gui_creating_key)); + } + + return view; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + getChildFragmentManager().putFragment(outState, ListFragment.class.getName(), mListFragment); + } + + /** + * Sets adapter and empty text for {@link ListFragment} + * @param adapter Adapter to be used for {@link}ListFragment#setListAdapter} + * @param emptyText Resource id for text to be shown in the + * {@link ListFragment#setEmptyText(CharSequence)}. + */ + public void setListAdapter(ListAdapter adapter, int emptyText) { + mListFragment.setListAdapter(adapter); + + mLoading.setVisibility(View.INVISIBLE); + mListFragmentHolder.setVisibility(View.VISIBLE); + mListFragment.setEmptyText(getString(emptyText)); + } + + @Override + public void onStart() { + super.onStart(); + onApiAvailable(); + } + + /** + * Calls onInitAdapter if it has not yet been called, ListFragment is initialized, + * and SyncthingService is not null, and + */ + @Override + public void onApiAvailable() { + MainActivity activity = (MainActivity) getActivity(); + if (!mInitialized && getActivity() != null && + activity.getApi() != null && mListFragment != null) { + onInitAdapter(activity); + mInitialized = true; + } + } + + /** + * Called when the list adapter should be set. + * @param activity + */ + public abstract void onInitAdapter(MainActivity activity); + +} diff --git a/src/main/java/com/nutomic/syncthingandroid/gui/MainActivity.java b/src/main/java/com/nutomic/syncthingandroid/gui/MainActivity.java new file mode 100644 index 00000000..f0810b23 --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/gui/MainActivity.java @@ -0,0 +1,206 @@ +package com.nutomic.syncthingandroid.gui; + +import android.app.AlertDialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBar.Tab; +import android.support.v7.app.ActionBar.TabListener; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; + +import com.nutomic.syncthingandroid.R; +import com.nutomic.syncthingandroid.syncthing.RestApi; +import com.nutomic.syncthingandroid.syncthing.SyncthingService; +import com.nutomic.syncthingandroid.syncthing.SyncthingServiceBinder; + +/** + * Shows {@link RepositoriesFragment} and {@link NodesFragment} in different tabs. + */ +public class MainActivity extends ActionBarActivity + implements SyncthingService.OnWebGuiAvailableListener{ + + private SyncthingService mSyncthingService; + + private final ServiceConnection mSyncthingServiceConnection = new ServiceConnection() { + + public void onServiceConnected(ComponentName className, IBinder service) { + SyncthingServiceBinder binder = (SyncthingServiceBinder) service; + mSyncthingService = binder.getService(); + mSyncthingService.registerOnWebGuiAvailableListener(MainActivity.this); + } + + public void onServiceDisconnected(ComponentName className) { + mSyncthingService = null; + } + }; + + @Override + public void onWebGuiAvailable() { + mSyncthingService.getApi().registerOnApiAvailableListener(mRepositoriesFragment); + mSyncthingService.getApi().registerOnApiAvailableListener(mNodesFragment); + } + + private final FragmentStatePagerAdapter mSectionsPagerAdapter = + new FragmentStatePagerAdapter(getSupportFragmentManager()) { + + @Override + public Fragment getItem(int position) { + switch (position) { + case 0: return mRepositoriesFragment; + case 1: return mNodesFragment; + default: return null; + } + } + + @Override + public int getCount() { + return 2; + } + + }; + + private RepositoriesFragment mRepositoriesFragment; + + private NodesFragment mNodesFragment; + + ViewPager mViewPager; + + /** + * Initializes tab navigation. + */ + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final ActionBar actionBar = getSupportActionBar(); + + actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); + setContentView(R.layout.main_activity); + + mViewPager = (ViewPager) findViewById(R.id.pager); + mViewPager.setAdapter(mSectionsPagerAdapter); + mViewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + actionBar.setSelectedNavigationItem(position); + } + }); + + TabListener tabListener = new TabListener() { + public void onTabSelected(Tab tab, FragmentTransaction ft) { + mViewPager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabReselected(Tab tab, FragmentTransaction ft) { + } + + @Override + public void onTabUnselected(Tab tab, FragmentTransaction ft) { + } + }; + + actionBar.addTab(actionBar.newTab() + .setText(R.string.repositories_fragment_title) + .setTabListener(tabListener)); + actionBar.addTab(actionBar.newTab() + .setText(R.string.nodes_fragment_title) + .setTabListener(tabListener)); + + if (savedInstanceState != null) { + FragmentManager fm = getSupportFragmentManager(); + mRepositoriesFragment = (RepositoriesFragment) fm.getFragment( + savedInstanceState, RepositoriesFragment.class.getName()); + mNodesFragment = (NodesFragment) fm.getFragment( + savedInstanceState, NodesFragment.class.getName()); + mViewPager.setCurrentItem(savedInstanceState.getInt("currentTab")); + } + else { + mRepositoriesFragment = new RepositoriesFragment(); + mNodesFragment = new NodesFragment(); + } + + if (SyncthingService.isFirstStart(this)) { + new AlertDialog.Builder(this) + .setTitle(R.string.welcome_title) + .setMessage(R.string.welcome_text) + .setNeutralButton(android.R.string.ok, null) + .show(); + } + + getApplicationContext().startService( + new Intent(this, SyncthingService.class)); + bindService(new Intent(this, SyncthingService.class), + mSyncthingServiceConnection, Context.BIND_AUTO_CREATE); + } + + @Override + public void onDestroy() { + super.onDestroy(); + unbindService(mSyncthingServiceConnection); + } + + /** + * Saves fragment states. + */ + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + // Avoid crash if called during startup. + if (mRepositoriesFragment != null && mNodesFragment != null) { + FragmentManager fm = getSupportFragmentManager(); + fm.putFragment(outState, RepositoriesFragment.class.getName(), mRepositoriesFragment); + fm.putFragment(outState, NodesFragment.class.getName(), mNodesFragment); + outState.putInt("currentTab", mViewPager.getCurrentItem()); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + return mSyncthingService != null && mSyncthingService.isWebGuiAvailable(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.web_gui: + startActivity(new Intent(this, WebGuiActivity.class)); + return true; + case R.id.settings: + startActivity(new Intent(this, SettingsActivity.class)); + return true; + case R.id.exit: + // Make sure we unbind first. + finish(); + getApplicationContext().stopService(new Intent(this, SyncthingService.class)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Returns RestApi instance, or null if SyncthingService is not yet connected. + */ + public RestApi getApi() { + return (mSyncthingService != null) + ? mSyncthingService.getApi() + : null; + } + +} diff --git a/src/main/java/com/nutomic/syncthingandroid/gui/NodesFragment.java b/src/main/java/com/nutomic/syncthingandroid/gui/NodesFragment.java new file mode 100644 index 00000000..addd4e32 --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/gui/NodesFragment.java @@ -0,0 +1,20 @@ +package com.nutomic.syncthingandroid.gui; + +import com.nutomic.syncthingandroid.NodeAdapter; +import com.nutomic.syncthingandroid.R; +import com.nutomic.syncthingandroid.syncthing.RestApi; + +/** + * Displays a list of all existing nodes. + */ +public class NodesFragment extends LoadingListFragment implements + RestApi.OnApiAvailableListener { + + @Override + public void onInitAdapter(MainActivity activity) { + NodeAdapter adapter = new NodeAdapter(activity); + adapter.add(activity.getApi().getNodes()); + setListAdapter(adapter, R.string.nodes_list_empty); + } + +} diff --git a/src/main/java/com/nutomic/syncthingandroid/gui/RepositoriesFragment.java b/src/main/java/com/nutomic/syncthingandroid/gui/RepositoriesFragment.java new file mode 100644 index 00000000..58b194c4 --- /dev/null +++ b/src/main/java/com/nutomic/syncthingandroid/gui/RepositoriesFragment.java @@ -0,0 +1,20 @@ +package com.nutomic.syncthingandroid.gui; + +import com.nutomic.syncthingandroid.R; +import com.nutomic.syncthingandroid.RepositoryAdapter; +import com.nutomic.syncthingandroid.syncthing.RestApi; + +/** + * Displays a list of all existing repositories. + */ +public class RepositoriesFragment extends LoadingListFragment implements + RestApi.OnApiAvailableListener { + + @Override + public void onInitAdapter(MainActivity activity) { + RepositoryAdapter adapter = new RepositoryAdapter(activity); + adapter.add(activity.getApi().getRepositories()); + setListAdapter(adapter, R.string.repositories_list_empty); + } + +} diff --git a/src/main/java/com/nutomic/syncthingandroid/gui/SettingsActivity.java b/src/main/java/com/nutomic/syncthingandroid/gui/SettingsActivity.java index f0d4ba3a..33bb0051 100644 --- a/src/main/java/com/nutomic/syncthingandroid/gui/SettingsActivity.java +++ b/src/main/java/com/nutomic/syncthingandroid/gui/SettingsActivity.java @@ -5,7 +5,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.IBinder; @@ -43,7 +42,7 @@ public class SettingsActivity extends PreferenceActivity /** * Binds to service and sets syncthing preferences from Rest API. */ - private ServiceConnection mSyncthingServiceConnection = new ServiceConnection() { + private final ServiceConnection mSyncthingServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { SyncthingServiceBinder binder = (SyncthingServiceBinder) service; diff --git a/src/main/java/com/nutomic/syncthingandroid/gui/WebGuiActivity.java b/src/main/java/com/nutomic/syncthingandroid/gui/WebGuiActivity.java index 2407a997..9456e2a8 100644 --- a/src/main/java/com/nutomic/syncthingandroid/gui/WebGuiActivity.java +++ b/src/main/java/com/nutomic/syncthingandroid/gui/WebGuiActivity.java @@ -1,16 +1,13 @@ package com.nutomic.syncthingandroid.gui; import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; -import android.view.Menu; -import android.view.MenuItem; +import android.support.v7.app.ActionBarActivity; import android.view.View; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -24,16 +21,15 @@ import com.nutomic.syncthingandroid.syncthing.SyncthingServiceBinder; /** * Holds a WebView that shows the web ui of the local syncthing instance. */ -public class WebGuiActivity extends Activity implements SyncthingService.OnWebGuiAvailableListener { - - private static final String TAG = "WebGuiActivity"; +public class WebGuiActivity extends ActionBarActivity implements SyncthingService.OnWebGuiAvailableListener { private WebView mWebView; + private View mLoadingView; private SyncthingService mSyncthingService; - private ServiceConnection mSyncthingServiceConnection = new ServiceConnection() { + private final ServiceConnection mSyncthingServiceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { SyncthingServiceBinder binder = (SyncthingServiceBinder) service; @@ -49,7 +45,7 @@ public class WebGuiActivity extends Activity implements SyncthingService.OnWebGu /** * Hides the loading screen and shows the WebView once it is fully loaded. */ - private WebViewClient mWebViewClient = new WebViewClient() { + private final WebViewClient mWebViewClient = new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { @@ -68,7 +64,8 @@ public class WebGuiActivity extends Activity implements SyncthingService.OnWebGu public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.main); + setContentView(R.layout.web_gui_activity); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); mLoadingView = findViewById(R.id.loading); ProgressBar pb = (ProgressBar) mLoadingView.findViewById(R.id.progress); @@ -81,15 +78,8 @@ public class WebGuiActivity extends Activity implements SyncthingService.OnWebGu if (SyncthingService.isFirstStart(this)) { TextView loadingText = (TextView) mLoadingView.findViewById(R.id.loading_text); loadingText.setText(R.string.web_gui_creating_key); - new AlertDialog.Builder(this) - .setTitle(R.string.welcome_title) - .setMessage(R.string.welcome_text) - .setNeutralButton(android.R.string.ok, null) - .show(); } - getApplicationContext().startService( - new Intent(this, SyncthingService.class)); bindService( new Intent(this, SyncthingService.class), mSyncthingServiceConnection, Context.BIND_AUTO_CREATE); @@ -108,32 +98,5 @@ public class WebGuiActivity extends Activity implements SyncthingService.OnWebGu super.onDestroy(); unbindService(mSyncthingServiceConnection); } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu, menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - return mSyncthingService != null && mSyncthingService.isWebGuiAvailable(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.settings: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - case R.id.exit: - // Make sure we unbind first. - finish(); - getApplicationContext().stopService(new Intent(this, SyncthingService.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java index e0aba1d9..939aa114 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/GetTask.java @@ -39,6 +39,7 @@ public class GetTask extends AsyncTask { HttpClient httpclient = new DefaultHttpClient(); HttpGet get = new HttpGet(fullUri); get.addHeader(new BasicHeader("X-API-Key", params[2])); + try { HttpResponse response = httpclient.execute(get); HttpEntity entity = response.getEntity(); diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java index f4728573..ea3aef65 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/PostTask.java @@ -3,7 +3,6 @@ package com.nutomic.syncthingandroid.syncthing; import android.os.AsyncTask; import android.util.Log; -import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; @@ -38,6 +37,7 @@ public class PostTask extends AsyncTask { HttpClient httpclient = new DefaultHttpClient(); HttpPost post = new HttpPost(fullUri); post.addHeader(new BasicHeader("X-API-Key", params[2])); + try { post.setEntity(new StringEntity(params[3])); httpclient.execute(post); diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java index add8630b..d635ecd5 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/RestApi.java @@ -14,6 +14,12 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + /** * Provides functions to interact with the syncthing REST API. */ @@ -31,19 +37,62 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { */ public static final String TYPE_GUI = "GUI"; + public static class Node { + public String Addresses; + public String Name; + public String NodeID; + } + + public static class Repository { + public String Directory; + public String ID; + public final boolean IgnorePerms = true; + public String Invalid; + public List Nodes; + public boolean ReadOnly; + public Versioning Versioning; + } + + public static class Versioning { + protected final Map mParams = new HashMap(); + public String getType() { + return ""; + } + public Map getParams() { + return mParams; + } + } + + public static class SimpleVersioning extends Versioning { + @Override + public String getType() { + return "simple"; + } + public void setParams(int keep) { + mParams.put("keep", Integer.toString(keep)); + } + } + + public interface OnApiAvailableListener { + public void onApiAvailable(); + } + + private final LinkedList mOnApiAvailableListeners = + new LinkedList(); + private static final int NOTIFICATION_RESTART = 2; - private Context mContext; + private final Context mContext; private String mVersion; - private String mUrl; + private final String mUrl; private String mApiKey; private JSONObject mConfig; - private NotificationManager mNotificationManager; + private final NotificationManager mNotificationManager; public RestApi(Context context, String url, String apiKey) { mContext = context; @@ -60,6 +109,9 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { return mUrl; } + /** + * Gets version and config, then calls any OnApiAvailableListeners. + */ @Override public void onWebGuiAvailable() { new GetTask() { @@ -74,6 +126,10 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { protected void onPostExecute(String config) { try { mConfig = new JSONObject(config); + for (OnApiAvailableListener listener : mOnApiAvailableListeners) { + listener.onApiAvailable(); + } + mOnApiAvailableListeners.clear(); } catch (JSONException e) { Log.w(TAG, "Failed to parse config", e); @@ -139,16 +195,9 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { */ public void setValue(String name, String key, T value, boolean isArray) { try { - if (isArray) { - JSONArray json = new JSONArray(); - for (String s : ((String) value).split(" ")) { - json.put(s); - } - mConfig.getJSONObject(name).put(key, json); - } - else { - mConfig.getJSONObject(name).put(key, value); - } + mConfig.getJSONObject(name).put(key, (isArray) + ? listToJson(((String) value).split(" ")) + : value); configUpdated(); } catch (JSONException e) { @@ -156,6 +205,18 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { } } + /** + * Converts an array of strings to JSON array. Like JSONArray#JSONArray(Object array), but + * works on all API levels. + */ + private JSONArray listToJson(String[] list) { + JSONArray json = new JSONArray(); + for (String s : list) { + json.put(s); + } + return json; + } + /** * Sends the updated mConfig via Rest API to syncthing and displays a "restart" notification. */ @@ -176,4 +237,90 @@ public class RestApi implements SyncthingService.OnWebGuiAvailableListener { mNotificationManager.notify(NOTIFICATION_RESTART, n); } + /** + * Returns a list of all existing nodes. + */ + public List getNodes() { + try { + return getNodes(mConfig.getJSONArray("Nodes")); + } + catch (JSONException e) { + Log.w(TAG, "Failed to read nodes", e); + return null; + } + } + + /** + * Returns a list of all nodes in the array nodes. + */ + private List getNodes(JSONArray nodes) throws JSONException { + List ret; + ret = new ArrayList(nodes.length()); + for (int i = 0; i < nodes.length(); i++) { + JSONObject json = nodes.getJSONObject(i); + Node n = new Node(); + if (!json.isNull("Addresses")) { + n.Addresses = json.getJSONArray("Addresses").join(" ").replace("\"", ""); + } + n.Name = json.getString("Name"); + n.NodeID = json.getString("NodeID"); + ret.add(n); + } + return ret; + } + + /** + * Returns a list of all existing repositores. + */ + public List getRepositories() { + List ret = null; + try { + JSONArray repos = mConfig.getJSONArray("Repositories"); + ret = new ArrayList(repos.length()); + for (int i = 0; i < repos.length(); i++) { + JSONObject json = repos.getJSONObject(i); + Repository r = new Repository(); + r.Directory = json.getString("Directory"); + r.ID = json.getString("ID"); + // Hardcoded to true because missing permissions support. + // r.IgnorePerms = json.getBoolean("IgnorePerms"); + r.Invalid = json.getString("Invalid"); + r.Nodes = getNodes(json.getJSONArray("Nodes")); + + r.ReadOnly = json.getBoolean("ReadOnly"); + JSONObject versioning = json.getJSONObject("Versioning"); + if (versioning.getString("Type").equals("simple")) { + SimpleVersioning sv = new SimpleVersioning(); + JSONObject params = versioning.getJSONObject("Params"); + sv.setParams(params.getInt("keep")); + r.Versioning = sv; + } + else { + r.Versioning = new Versioning(); + } + + ret.add(r); + } + } + catch (JSONException e) { + Log.w(TAG, "Failed to read nodes", e); + } + return ret; + } + + /** + * Register a listener for the web gui becoming available.. + * + * If the web gui is already available, listener will be called immediately. + * Listeners are unregistered automatically after being called. + */ + public void registerOnApiAvailableListener(OnApiAvailableListener listener) { + if (mConfig != null) { + listener.onApiAvailable(); + } + else { + mOnApiAvailableListeners.addLast(listener); + } + } + } diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java index b3aaeca9..90b9b9b2 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingService.java @@ -14,7 +14,7 @@ import android.util.Log; import android.util.Pair; import com.nutomic.syncthingandroid.R; -import com.nutomic.syncthingandroid.gui.WebGuiActivity; +import com.nutomic.syncthingandroid.gui.MainActivity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; @@ -104,7 +104,7 @@ public class SyncthingService extends Service { public void onWebGuiAvailable(); } - private LinkedList mOnWebGuiAvailableListeners = + private final LinkedList mOnWebGuiAvailableListeners = new LinkedList(); private boolean mIsWebGuiAvailable = false; @@ -292,10 +292,9 @@ public class SyncthingService extends Service { * Creates notification, starts native binary. */ @Override - public void onCreate() { PendingIntent pi = PendingIntent.getActivity( - this, 0, new Intent(this, WebGuiActivity.class), + this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); Notification n = new NotificationCompat.Builder(this) .setContentTitle(getString(R.string.app_name)) @@ -498,7 +497,7 @@ public class SyncthingService extends Service { in = getResources().openRawResource(R.raw.config_default); out = new FileOutputStream(getConfigFile()); byte[] buff = new byte[1024]; - int read = 0; + int read; while ((read = in.read(buff)) > 0) { out.write(buff, 0, read); diff --git a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingServiceBinder.java b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingServiceBinder.java index a9b3a375..b1a56224 100644 --- a/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingServiceBinder.java +++ b/src/main/java/com/nutomic/syncthingandroid/syncthing/SyncthingServiceBinder.java @@ -4,7 +4,7 @@ import android.os.Binder; public class SyncthingServiceBinder extends Binder { - SyncthingService mService; + private final SyncthingService mService; public SyncthingServiceBinder(SyncthingService service) { mService = service; diff --git a/src/main/res/layout/loading_list_fragment.xml b/src/main/res/layout/loading_list_fragment.xml new file mode 100644 index 00000000..3aeb0bb6 --- /dev/null +++ b/src/main/res/layout/loading_list_fragment.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/main_activity.xml b/src/main/res/layout/main_activity.xml new file mode 100644 index 00000000..31185efc --- /dev/null +++ b/src/main/res/layout/main_activity.xml @@ -0,0 +1,7 @@ + + + diff --git a/src/main/res/layout/node_list_item.xml b/src/main/res/layout/node_list_item.xml new file mode 100644 index 00000000..54772689 --- /dev/null +++ b/src/main/res/layout/node_list_item.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/repository_list_item.xml b/src/main/res/layout/repository_list_item.xml new file mode 100644 index 00000000..1609afe3 --- /dev/null +++ b/src/main/res/layout/repository_list_item.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/main.xml b/src/main/res/layout/web_gui_activity.xml similarity index 100% rename from src/main/res/layout/main.xml rename to src/main/res/layout/web_gui_activity.xml diff --git a/src/main/res/menu/menu.xml b/src/main/res/menu/menu.xml index 6131052c..6372450a 100644 --- a/src/main/res/menu/menu.xml +++ b/src/main/res/menu/menu.xml @@ -1,6 +1,10 @@ + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 808e3151..5fb61b8f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -9,8 +9,25 @@ Click here to restart syncthing now + + + Repositories + + + No repositories found + + + + Nodes + + + No nodes found + + + Web GUI + Waiting for GUI diff --git a/src/main/res/xml/settings.xml b/src/main/res/xml/settings.xml index d7485598..dc3225d3 100644 --- a/src/main/res/xml/settings.xml +++ b/src/main/res/xml/settings.xml @@ -2,79 +2,79 @@ + android:key="syncthing_options" + android:title="Syncthing Options" + android:persistent="false" > + android:key="ListenAddress" + android:title="Sync Protocol Listen Addresses" + android:defaultValue="0.0.0.0:22000" /> + android:key="MaxSendKbps" + android:title="Outgoing Rate Limit (KiB/s)" + android:numeric="integer" /> + android:key="RescanIntervalS" + android:title="Rescan Interval (s)" + android:numeric="integer" /> + android:key="ReconnectIntervalS" + android:title="Reconnect Interval (s)" + android:numeric="integer" /> + android:key="ParallelRequests" + android:title="Max Outstanding Requests" + android:numeric="integer" /> + android:key="MaxChangeKbps" + android:title="Max File Change Rate (KiB/s)" + android:numeric="integer" /> + android:key="GlobalAnnEnabled" + android:title="Global Discovery" /> + android:key="LocalAnnEnabled" + android:title="Local Discovery" /> + android:key="LocalAnnPort" + android:title="Local Discovery Port" + android:numeric="integer" /> + android:key="UPnPEnabled" + android:title="Enable UPnP" /> + android:key="syncthing_gui" + android:title="Syncthing GUI" + android:persistent="false" > + android:key="Address" + android:title="GUI Listen Addresses" /> + android:key="User" + android:title="GUI Authentication User" /> + android:key="Password" + android:title="GUI Authentication Password" /> + android:key="UseTLS" + android:title="Use HTTPS for GUI" /> @@ -87,8 +87,8 @@ + android:key="syncthing_version" + android:title="@string/syncthing_version_title" + style="?android:preferenceInformationStyle" />