diff --git a/docs-android/app/app.iml b/docs-android/app/app.iml index 42f9493f..8e392ec5 100644 --- a/docs-android/app/app.iml +++ b/docs-android/app/app.iml @@ -70,7 +70,10 @@ + + + diff --git a/docs-android/app/build.gradle b/docs-android/app/build.gradle index d1e5d434..29fd4179 100644 --- a/docs-android/app/build.gradle +++ b/docs-android/app/build.gradle @@ -36,5 +36,8 @@ android { } dependencies { + compile fileTree(dir: 'libs', include: '*.jar') compile 'com.android.support:support-v4:19.1.0' + compile 'ch.acra:acra:4.5.0' + compile 'com.loopj.android:android-async-http:1.4.4' } diff --git a/docs-android/app/libs/android-query.0.26.8.jar b/docs-android/app/libs/android-query.0.26.8.jar new file mode 100644 index 00000000..7fc26f17 Binary files /dev/null and b/docs-android/app/libs/android-query.0.26.8.jar differ diff --git a/docs-android/app/src/main/AndroidManifest.xml b/docs-android/app/src/main/AndroidManifest.xml index 86960760..62370917 100644 --- a/docs-android/app/src/main/AndroidManifest.xml +++ b/docs-android/app/src/main/AndroidManifest.xml @@ -2,27 +2,31 @@ + + + + + - + - + android:name=".activity.MainActivity" + android:label="@string/app_name" + android:logo="@drawable/ic_launcher" + android:launchMode="singleTop"> diff --git a/docs-android/app/src/main/java/com/sismics/docs/DocDetailActivity.java b/docs-android/app/src/main/java/com/sismics/docs/DocDetailActivity.java deleted file mode 100644 index 0ba834eb..00000000 --- a/docs-android/app/src/main/java/com/sismics/docs/DocDetailActivity.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.sismics.docs; - -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.NavUtils; -import android.view.MenuItem; - -/** - * An activity representing a single Doc detail screen. This - * activity is only used on handset devices. On tablet-size devices, - * item details are presented side-by-side with a list of items - * in a {@link DocListActivity}. - *

- * This activity is mostly just a 'shell' activity containing nothing - * more than a {@link DocDetailFragment}. - */ -public class DocDetailActivity extends FragmentActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_doc_detail); - - // Show the Up button in the action bar. - getActionBar().setDisplayHomeAsUpEnabled(true); - - // savedInstanceState is non-null when there is fragment state - // saved from previous configurations of this activity - // (e.g. when rotating the screen from portrait to landscape). - // In this case, the fragment will automatically be re-added - // to its container so we don't need to manually add it. - // For more information, see the Fragments API guide at: - // - // http://developer.android.com/guide/components/fragments.html - // - if (savedInstanceState == null) { - // Create the detail fragment and add it to the activity - // using a fragment transaction. - Bundle arguments = new Bundle(); - arguments.putString(DocDetailFragment.ARG_ITEM_ID, - getIntent().getStringExtra(DocDetailFragment.ARG_ITEM_ID)); - DocDetailFragment fragment = new DocDetailFragment(); - fragment.setArguments(arguments); - getSupportFragmentManager().beginTransaction() - .add(R.id.doc_detail_container, fragment) - .commit(); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - if (id == android.R.id.home) { - // This ID represents the Home or Up button. In the case of this - // activity, the Up button is shown. Use NavUtils to allow users - // to navigate up one level in the application structure. For - // more details, see the Navigation pattern on Android Design: - // - // http://developer.android.com/design/patterns/navigation.html#up-vs-back - // - NavUtils.navigateUpTo(this, new Intent(this, DocListActivity.class)); - return true; - } - return super.onOptionsItemSelected(item); - } -} diff --git a/docs-android/app/src/main/java/com/sismics/docs/DocDetailFragment.java b/docs-android/app/src/main/java/com/sismics/docs/DocDetailFragment.java deleted file mode 100644 index eae4abb4..00000000 --- a/docs-android/app/src/main/java/com/sismics/docs/DocDetailFragment.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.sismics.docs; - -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.sismics.docs.dummy.DummyContent; - -/** - * A fragment representing a single Doc detail screen. - * This fragment is either contained in a {@link DocListActivity} - * in two-pane mode (on tablets) or a {@link DocDetailActivity} - * on handsets. - */ -public class DocDetailFragment extends Fragment { - /** - * The fragment argument representing the item ID that this fragment - * represents. - */ - public static final String ARG_ITEM_ID = "item_id"; - - /** - * The dummy content this fragment is presenting. - */ - private DummyContent.DummyItem mItem; - - /** - * Mandatory empty constructor for the fragment manager to instantiate the - * fragment (e.g. upon screen orientation changes). - */ - public DocDetailFragment() { - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (getArguments().containsKey(ARG_ITEM_ID)) { - // Load the dummy content specified by the fragment - // arguments. In a real-world scenario, use a Loader - // to load content from a content provider. - mItem = DummyContent.ITEM_MAP.get(getArguments().getString(ARG_ITEM_ID)); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_doc_detail, container, false); - - // Show the dummy content as text in a TextView. - if (mItem != null) { - ((TextView) rootView.findViewById(R.id.doc_detail)).setText(mItem.content); - } - - return rootView; - } -} diff --git a/docs-android/app/src/main/java/com/sismics/docs/DocListActivity.java b/docs-android/app/src/main/java/com/sismics/docs/DocListActivity.java deleted file mode 100644 index 6e0a15a8..00000000 --- a/docs-android/app/src/main/java/com/sismics/docs/DocListActivity.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.sismics.docs; - -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.FragmentActivity; - - -/** - * An activity representing a list of Docs. This activity - * has different presentations for handset and tablet-size devices. On - * handsets, the activity presents a list of items, which when touched, - * lead to a {@link DocDetailActivity} representing - * item details. On tablets, the activity presents the list of items and - * item details side-by-side using two vertical panes. - *

- * The activity makes heavy use of fragments. The list of items is a - * {@link DocListFragment} and the item details - * (if present) is a {@link DocDetailFragment}. - *

- * This activity also implements the required - * {@link DocListFragment.Callbacks} interface - * to listen for item selections. - */ -public class DocListActivity extends FragmentActivity - implements DocListFragment.Callbacks { - - /** - * Whether or not the activity is in two-pane mode, i.e. running on a tablet - * device. - */ - private boolean mTwoPane; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_doc_list); - - if (findViewById(R.id.doc_detail_container) != null) { - // The detail container view will be present only in the - // large-screen layouts (res/values-large and - // res/values-sw600dp). If this view is present, then the - // activity should be in two-pane mode. - mTwoPane = true; - - // In two-pane mode, list items should be given the - // 'activated' state when touched. - ((DocListFragment) getSupportFragmentManager() - .findFragmentById(R.id.doc_list)) - .setActivateOnItemClick(true); - } - - // TODO: If exposing deep links into your app, handle intents here. - } - - /** - * Callback method from {@link DocListFragment.Callbacks} - * indicating that the item with the given ID was selected. - */ - @Override - public void onItemSelected(String id) { - if (mTwoPane) { - // In two-pane mode, show the detail view in this activity by - // adding or replacing the detail fragment using a - // fragment transaction. - Bundle arguments = new Bundle(); - arguments.putString(DocDetailFragment.ARG_ITEM_ID, id); - DocDetailFragment fragment = new DocDetailFragment(); - fragment.setArguments(arguments); - getSupportFragmentManager().beginTransaction() - .replace(R.id.doc_detail_container, fragment) - .commit(); - - } else { - // In single-pane mode, simply start the detail activity - // for the selected item ID. - Intent detailIntent = new Intent(this, DocDetailActivity.class); - detailIntent.putExtra(DocDetailFragment.ARG_ITEM_ID, id); - startActivity(detailIntent); - } - } -} diff --git a/docs-android/app/src/main/java/com/sismics/docs/DocListFragment.java b/docs-android/app/src/main/java/com/sismics/docs/DocListFragment.java deleted file mode 100644 index eac1ab18..00000000 --- a/docs-android/app/src/main/java/com/sismics/docs/DocListFragment.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.sismics.docs; - -import android.app.Activity; -import android.os.Bundle; -import android.support.v4.app.ListFragment; -import android.view.View; -import android.widget.ArrayAdapter; -import android.widget.ListView; - -import com.sismics.docs.dummy.DummyContent; - -/** - * A list fragment representing a list of Docs. This fragment - * also supports tablet devices by allowing list items to be given an - * 'activated' state upon selection. This helps indicate which item is - * currently being viewed in a {@link DocDetailFragment}. - *

- * Activities containing this fragment MUST implement the {@link Callbacks} - * interface. - */ -public class DocListFragment extends ListFragment { - - /** - * The serialization (saved instance state) Bundle key representing the - * activated item position. Only used on tablets. - */ - private static final String STATE_ACTIVATED_POSITION = "activated_position"; - - /** - * The fragment's current callback object, which is notified of list item - * clicks. - */ - private Callbacks mCallbacks = sDummyCallbacks; - - /** - * The current activated item position. Only used on tablets. - */ - private int mActivatedPosition = ListView.INVALID_POSITION; - - /** - * A callback interface that all activities containing this fragment must - * implement. This mechanism allows activities to be notified of item - * selections. - */ - public interface Callbacks { - /** - * Callback for when an item has been selected. - */ - public void onItemSelected(String id); - } - - /** - * A dummy implementation of the {@link Callbacks} interface that does - * nothing. Used only when this fragment is not attached to an activity. - */ - private static Callbacks sDummyCallbacks = new Callbacks() { - @Override - public void onItemSelected(String id) { - } - }; - - /** - * Mandatory empty constructor for the fragment manager to instantiate the - * fragment (e.g. upon screen orientation changes). - */ - public DocListFragment() { - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // TODO: replace with a real list adapter. - setListAdapter(new ArrayAdapter( - getActivity(), - android.R.layout.simple_list_item_activated_1, - android.R.id.text1, - DummyContent.ITEMS)); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - // Restore the previously serialized activated item position. - if (savedInstanceState != null - && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { - setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); - } - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - // Activities containing this fragment must implement its callbacks. - if (!(activity instanceof Callbacks)) { - throw new IllegalStateException("Activity must implement fragment's callbacks."); - } - - mCallbacks = (Callbacks) activity; - } - - @Override - public void onDetach() { - super.onDetach(); - - // Reset the active callbacks interface to the dummy implementation. - mCallbacks = sDummyCallbacks; - } - - @Override - public void onListItemClick(ListView listView, View view, int position, long id) { - super.onListItemClick(listView, view, position, id); - - // Notify the active callbacks interface (the activity, if the - // fragment is attached to one) that an item has been selected. - mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (mActivatedPosition != ListView.INVALID_POSITION) { - // Serialize and persist the activated item position. - outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition); - } - } - - /** - * Turns on activate-on-click mode. When this mode is on, list items will be - * given the 'activated' state when touched. - */ - public void setActivateOnItemClick(boolean activateOnItemClick) { - // When setting CHOICE_MODE_SINGLE, ListView will automatically - // give items the 'activated' state when touched. - getListView().setChoiceMode(activateOnItemClick - ? ListView.CHOICE_MODE_SINGLE - : ListView.CHOICE_MODE_NONE); - } - - private void setActivatedPosition(int position) { - if (position == ListView.INVALID_POSITION) { - getListView().setItemChecked(mActivatedPosition, false); - } else { - getListView().setItemChecked(position, true); - } - - mActivatedPosition = position; - } -} diff --git a/docs-android/app/src/main/java/com/sismics/docs/MainApplication.java b/docs-android/app/src/main/java/com/sismics/docs/MainApplication.java new file mode 100644 index 00000000..805ae8a0 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/MainApplication.java @@ -0,0 +1,46 @@ +package com.sismics.docs; + +import android.app.Application; + +import com.androidquery.callback.BitmapAjaxCallback; +import com.sismics.docs.model.application.ApplicationContext; +import com.sismics.docs.util.PreferenceUtil; + +import org.acra.ACRA; +import org.acra.ReportingInteractionMode; +import org.acra.annotation.ReportsCrashes; +import org.acra.sender.HttpSender.Method; +import org.acra.sender.HttpSender.Type; +import org.json.JSONObject; + +/** + * Main application. + * + * @author bgamard + */ +@ReportsCrashes(formKey = "", + httpMethod = Method.PUT, + reportType = Type.JSON, + formUri = "http://acralyzer.sismics.com/docs-report", + formUriBasicAuthLogin = "reporter", + formUriBasicAuthPassword = "jOS9ezJR", + mode = ReportingInteractionMode.TOAST, + forceCloseDialogAfterToast = true, + resToastText = R.string.crash_toast_text) +public class MainApplication extends Application { + @Override + public void onCreate() { + ACRA.init(this); + + // Fetching /user/info from cache + JSONObject json = PreferenceUtil.getCachedJson(getApplicationContext(), PreferenceUtil.PREF_CACHED_USER_INFO_JSON); + ApplicationContext.getInstance().setUserInfo(getApplicationContext(), json); + + super.onCreate(); + } + + @Override + public void onLowMemory() { + BitmapAjaxCallback.clearCache(); + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/activity/LoginActivity.java b/docs-android/app/src/main/java/com/sismics/docs/activity/LoginActivity.java new file mode 100644 index 00000000..cf5a92bf --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/activity/LoginActivity.java @@ -0,0 +1,184 @@ +package com.sismics.docs.activity; + +import android.content.Intent; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v4.app.FragmentActivity; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; + +import com.androidquery.AQuery; +import com.loopj.android.http.JsonHttpResponseHandler; +import com.sismics.docs.R; +import com.sismics.docs.listener.CallbackListener; +import com.sismics.docs.model.application.ApplicationContext; +import com.sismics.docs.resource.UserResource; +import com.sismics.docs.ui.form.Validator; +import com.sismics.docs.ui.form.validator.Required; +import com.sismics.docs.util.DialogUtil; +import com.sismics.docs.util.PreferenceUtil; + +import org.apache.http.Header; +import org.json.JSONObject; + +/** + * Login activity. + * + * @author bgamard + */ +public class LoginActivity extends FragmentActivity { + + /** + * User interface. + */ + private View loginForm; + private View progressBar; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.login_activity); + + AQuery aq = new AQuery(this); + aq.id(R.id.loginExplain) + .text(Html.fromHtml(getString(R.string.login_explain))) + .getTextView() + .setMovementMethod(LinkMovementMethod.getInstance()); + + final EditText txtServer = aq.id(R.id.txtServer).getEditText(); + final EditText txtUsername = aq.id(R.id.txtUsername).getEditText(); + final EditText txtPassword = aq.id(R.id.txtPassword).getEditText(); + final Button btnConnect = aq.id(R.id.btnConnect).getButton(); + loginForm = aq.id(R.id.loginForm).getView(); + progressBar = aq.id(R.id.progressBar).getView(); + + PreferenceManager.setDefaultValues(this, R.xml.preferences, false); + + loginForm.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + + // Form validation + final Validator validator = new Validator(false); + validator.addValidable(this, txtServer, new Required()); + validator.addValidable(this, txtUsername, new Required()); + validator.addValidable(this, txtPassword, new Required()); + validator.setOnValidationChanged(new CallbackListener() { + @Override + public void onComplete() { + btnConnect.setEnabled(validator.isValidated()); + } + }); + + // Preset saved server URL + String serverUrl = PreferenceUtil.getStringPreference(this, PreferenceUtil.PREF_SERVER_URL); + if (serverUrl != null) { + txtServer.setText(serverUrl); + } + + tryConnect(); + + // Login button + btnConnect.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + loginForm.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + + PreferenceUtil.setServerUrl(LoginActivity.this, txtServer.getText().toString()); + + try { + UserResource.login(getApplicationContext(), txtUsername.getText().toString(), txtPassword.getText().toString(), new JsonHttpResponseHandler() { + @Override + public void onSuccess(JSONObject json) { + // Empty previous user caches + PreferenceUtil.resetUserCache(getApplicationContext()); + + // Getting user info and redirecting to main activity + ApplicationContext.getInstance().fetchUserInfo(LoginActivity.this, new CallbackListener() { + @Override + public void onComplete() { + Intent intent = new Intent(LoginActivity.this, MainActivity.class); + startActivity(intent); + finish(); + } + }); + } + + @Override + public void onFailure(final int statusCode, final Header[] headers, final byte[] responseBytes, final Throwable throwable) { + loginForm.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + + if (responseBytes != null && new String(responseBytes).contains("\"ForbiddenError\"")) { + DialogUtil.showOkDialog(LoginActivity.this, R.string.login_fail_title, R.string.login_fail); + } else { + DialogUtil.showOkDialog(LoginActivity.this, R.string.network_error_title, R.string.network_error); + } + } + }); + } catch (IllegalArgumentException e) { + // Given URL is not valid + loginForm.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + PreferenceUtil.setServerUrl(LoginActivity.this, null); + DialogUtil.showOkDialog(LoginActivity.this, R.string.invalid_url_title, R.string.invalid_url); + } + } + }); + } + + /** + * Try to get a "session". + */ + private void tryConnect() { + String serverUrl = PreferenceUtil.getStringPreference(this, PreferenceUtil.PREF_SERVER_URL); + if (serverUrl == null) { + // Server URL is empty + loginForm.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + return; + } + + if (ApplicationContext.getInstance().isLoggedIn()) { + // If we are already connected (from cache data) + // redirecting to main activity + Intent intent = new Intent(LoginActivity.this, MainActivity.class); + startActivity(intent); + finish(); + } else { + // Trying to get user data + UserResource.info(getApplicationContext(), new JsonHttpResponseHandler() { + @Override + public void onSuccess(final JSONObject json) { + if (json.optBoolean("anonymous", true)) { + loginForm.setVisibility(View.VISIBLE); + return; + } + + // Save user data in application context + ApplicationContext.getInstance().setUserInfo(getApplicationContext(), json); + + // Redirecting to main activity + Intent intent = new Intent(LoginActivity.this, MainActivity.class); + startActivity(intent); + finish(); + } + + @Override + public void onFailure(final int statusCode, final Header[] headers, final byte[] responseBytes, final Throwable throwable) { + DialogUtil.showOkDialog(LoginActivity.this, R.string.network_error_title, R.string.network_error); + loginForm.setVisibility(View.VISIBLE); + } + + @Override + public void onFinish() { + progressBar.setVisibility(View.GONE); + } + }); + } + } +} \ No newline at end of file diff --git a/docs-android/app/src/main/java/com/sismics/docs/activity/MainActivity.java b/docs-android/app/src/main/java/com/sismics/docs/activity/MainActivity.java new file mode 100644 index 00000000..fb61adb1 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/activity/MainActivity.java @@ -0,0 +1,123 @@ +package com.sismics.docs.activity; + +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.v4.app.ActionBarDrawerToggle; +import android.support.v4.app.FragmentActivity; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.ListView; + +import com.sismics.docs.R; +import com.sismics.docs.model.application.ApplicationContext; + +/** + * Main activity. + * + * @author bgamard + */ +public class MainActivity extends FragmentActivity { + + private ListView drawerList; + private ActionBarDrawerToggle drawerToggle; + + @Override + protected void onCreate(final Bundle args) { + super.onCreate(args); + + // Check if logged in + if (!ApplicationContext.getInstance().isLoggedIn()) { + startActivity(new Intent(this, LoginActivity.class)); + finish(); + return; + } + + // Setup the activity + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.main_activity); + + // Cache view references + DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); + drawerList = (ListView) findViewById(R.id.drawer_list); + + // Drawer item click listener + drawerList.setOnItemClickListener(new ListView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + } + }); + + if (drawerLayout != null) { + // Set a custom shadow that overlays the main content when the drawer opens + drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); + + // Enable ActionBar app icon to behave as action to toggle nav drawer + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); + + // ActionBarDrawerToggle ties together the the proper interactions + // between the sliding drawer and the action bar app icon + drawerToggle = new ActionBarDrawerToggle( + this, /* host Activity */ + drawerLayout, /* DrawerLayout object */ + R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) { + + @Override + public void onDrawerOpened(View drawerView) { + invalidateOptionsMenu(); + } + + @Override + public void onDrawerClosed(View drawerView) { + invalidateOptionsMenu(); + } + }; + drawerLayout.setDrawerListener(drawerToggle); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + // The action bar home/up action should open or close the drawer. + // ActionBarDrawerToggle will take care of this. + if (drawerToggle != null && drawerToggle.onOptionsItemSelected(item)) { + return true; + } + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + if (drawerToggle != null) { + // Sync the toggle state after onRestoreInstanceState has occurred. + drawerToggle.syncState(); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (drawerToggle != null) { + // Pass any configuration change to the drawer toggle + drawerToggle.onConfigurationChanged(newConfig); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("drawerItemSelected", drawerList.getCheckedItemPosition()); + } +} \ No newline at end of file diff --git a/docs-android/app/src/main/java/com/sismics/docs/dummy/DummyContent.java b/docs-android/app/src/main/java/com/sismics/docs/dummy/DummyContent.java deleted file mode 100644 index ee227fc5..00000000 --- a/docs-android/app/src/main/java/com/sismics/docs/dummy/DummyContent.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.sismics.docs.dummy; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Helper class for providing sample content for user interfaces created by - * Android template wizards. - *

- * TODO: Replace all uses of this class before publishing your app. - */ -public class DummyContent { - - /** - * An array of sample (dummy) items. - */ - public static List ITEMS = new ArrayList(); - - /** - * A map of sample (dummy) items, by ID. - */ - public static Map ITEM_MAP = new HashMap(); - - static { - // Add 3 sample items. - addItem(new DummyItem("1", "Item 1")); - addItem(new DummyItem("2", "Item 2")); - addItem(new DummyItem("3", "Item 3")); - } - - private static void addItem(DummyItem item) { - ITEMS.add(item); - ITEM_MAP.put(item.id, item); - } - - /** - * A dummy item representing a piece of content. - */ - public static class DummyItem { - public String id; - public String content; - - public DummyItem(String id, String content) { - this.id = id; - this.content = content; - } - - @Override - public String toString() { - return content; - } - } -} diff --git a/docs-android/app/src/main/java/com/sismics/docs/listener/CallbackListener.java b/docs-android/app/src/main/java/com/sismics/docs/listener/CallbackListener.java new file mode 100644 index 00000000..8caf7265 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/listener/CallbackListener.java @@ -0,0 +1,10 @@ +package com.sismics.docs.listener; + +/** + * Simple listener. + * + * @author bgamard + */ +public interface CallbackListener { + public void onComplete(); +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/model/application/ApplicationContext.java b/docs-android/app/src/main/java/com/sismics/docs/model/application/ApplicationContext.java new file mode 100644 index 00000000..99625f22 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/model/application/ApplicationContext.java @@ -0,0 +1,99 @@ +package com.sismics.docs.model.application; + +import android.app.Activity; +import android.content.Context; + +import com.loopj.android.http.JsonHttpResponseHandler; +import com.sismics.docs.listener.CallbackListener; +import com.sismics.docs.resource.UserResource; +import com.sismics.docs.util.PreferenceUtil; + +import org.json.JSONObject; + +/** + * Global context of the application. + * + * @author bgamard + */ +public class ApplicationContext { + /** + * Singleton's instance. + */ + private static ApplicationContext applicationContext; + + /** + * Response of /user/info + */ + private JSONObject userInfo; + + /** + * Private constructor. + */ + private ApplicationContext() { + } + + /** + * Returns a singleton of ApplicationContext. + * + * @return Singleton of ApplicationContext + */ + public static ApplicationContext getInstance() { + if (applicationContext == null) { + applicationContext = new ApplicationContext(); + } + return applicationContext; + } + + /** + * Returns true if current user is logged in. + * + * @return + */ + public boolean isLoggedIn() { + return userInfo != null && !userInfo.optBoolean("anonymous"); + } + + /** + * Getter of userInfo + * + * @return + */ + public JSONObject getUserInfo() { + return userInfo; + } + + /** + * Setter of userInfo + * + * @param json + */ + public void setUserInfo(Context context, JSONObject json) { + this.userInfo = json; + PreferenceUtil.setCachedJson(context, PreferenceUtil.PREF_CACHED_USER_INFO_JSON, json); + } + + /** + * Asynchronously get user info. + * + * @param activity + * @param callbackListener + */ + public void fetchUserInfo(final Activity activity, final CallbackListener callbackListener) { + UserResource.info(activity.getApplicationContext(), new JsonHttpResponseHandler() { + @Override + public void onSuccess(final JSONObject json) { + // Save data in application context + if (!json.optBoolean("anonymous", true)) { + setUserInfo(activity.getApplicationContext(), json); + } + } + + @Override + public void onFinish() { + if (callbackListener != null) { + callbackListener.onComplete(); + } + } + }); + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/resource/BaseResource.java b/docs-android/app/src/main/java/com/sismics/docs/resource/BaseResource.java new file mode 100644 index 00000000..56933dd7 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/resource/BaseResource.java @@ -0,0 +1,137 @@ +package com.sismics.docs.resource; + +import android.content.Context; +import android.os.Build; + +import com.androidquery.callback.AbstractAjaxCallback; +import com.loopj.android.http.AsyncHttpClient; +import com.loopj.android.http.PersistentCookieStore; +import com.sismics.docs.util.ApplicationUtil; +import com.sismics.docs.util.PreferenceUtil; + +import org.apache.http.conn.ssl.SSLSocketFactory; + +import java.io.IOException; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Locale; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * Base class for API access. + * + * @author bgamard + */ +public class BaseResource { + + /** + * User-Agent to use. + */ + private static String USER_AGENT = null; + + /** + * Accept-Language header. + */ + private static String ACCEPT_LANGUAGE = null; + + /** + * HTTP client. + */ + protected static AsyncHttpClient client = new AsyncHttpClient(); + + static { + // 20sec default timeout + client.setTimeout(60000); + try { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + MySSLSocketFactory sf = new MySSLSocketFactory(trustStore); + sf.setHostnameVerifier(MySSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + client.setSSLSocketFactory(sf); + AbstractAjaxCallback.setSSF(sf); + } catch (Exception e) { + // NOP + } + } + + /** + * Resource initialization. + * @param context Context + */ + protected static void init(Context context) { + PersistentCookieStore cookieStore = new PersistentCookieStore(context); + client.setCookieStore(cookieStore); + + if (USER_AGENT == null) { + USER_AGENT = "Sismics Docs Android " + ApplicationUtil.getVersionName(context) + "/Android " + Build.VERSION.RELEASE + "/" + Build.MODEL; + client.setUserAgent(USER_AGENT); + } + + if (ACCEPT_LANGUAGE == null) { + Locale locale = Locale.getDefault(); + ACCEPT_LANGUAGE = locale.getLanguage() + "_" + locale.getCountry(); + client.addHeader("Accept-Language", ACCEPT_LANGUAGE); + } + } + + /** + * Socket factory to allow self-signed certificates. + * + * @author bgamard + */ + public static class MySSLSocketFactory extends SSLSocketFactory { + SSLContext sslContext = SSLContext.getInstance("TLS"); + + public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { + super(truststore); + + TrustManager tm = new X509TrustManager() { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + } + + public X509Certificate[] getAcceptedIssuers() { + return null; + } + }; + + sslContext.init(null, new TrustManager[] { tm }, null); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { + return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose); + } + + @Override + public Socket createSocket() throws IOException { + return sslContext.getSocketFactory().createSocket(); + } + } + + /** + * Returns cleaned API URL. + * @param context Context + * @return Cleaned API URL + */ + protected static String getApiUrl(Context context) { + String serverUrl = PreferenceUtil.getServerUrl(context); + + if (serverUrl == null) { + return null; + } + + return serverUrl + "/api"; + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/resource/UserResource.java b/docs-android/app/src/main/java/com/sismics/docs/resource/UserResource.java new file mode 100644 index 00000000..19e1ebe5 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/resource/UserResource.java @@ -0,0 +1,55 @@ +package com.sismics.docs.resource; + +import android.content.Context; + +import com.loopj.android.http.JsonHttpResponseHandler; +import com.loopj.android.http.RequestParams; + +/** + * Access to /user API. + * + * @author bgamard + */ +public class UserResource extends BaseResource { + + /** + * POST /user/login. + * @param context Context + * @param username Username + * @param password Password + * @param responseHandler Callback + */ + public static void login(Context context, String username, String password, JsonHttpResponseHandler responseHandler) { + init(context); + + RequestParams params = new RequestParams(); + params.put("username", username); + params.put("password", password); + params.put("remember", "true"); + client.post(getApiUrl(context) + "/user/login", params, responseHandler); + } + + /** + * GET /user. + * @param context Context + * @param responseHandler Callback + */ + public static void info(Context context, JsonHttpResponseHandler responseHandler) { + init(context); + + RequestParams params = new RequestParams(); + client.get(getApiUrl(context) + "/user", params, responseHandler); + } + + /** + * POST /user/logout. + * @param context Context + * @param responseHandler Callback + */ + public static void logout(Context context, JsonHttpResponseHandler responseHandler) { + init(context); + + RequestParams params = new RequestParams(); + client.post(getApiUrl(context) + "/user/logout", params, responseHandler); + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/ui/form/Validable.java b/docs-android/app/src/main/java/com/sismics/docs/ui/form/Validable.java new file mode 100644 index 00000000..9dd57d3f --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/ui/form/Validable.java @@ -0,0 +1,42 @@ +package com.sismics.docs.ui.form; + +import android.view.View; + +public class Validable { + + private View view; + + private boolean isValidated = false; + + /** + * Getter of view. + * @return view + */ + public View getView() { + return view; + } + + /** + * Setter of view. + * @param view view + */ + public void setView(View view) { + this.view = view; + } + + /** + * Getter of isValidated. + * @return isValidated + */ + public boolean isValidated() { + return isValidated; + } + + /** + * Setter of isValidated. + * @param isValidated isValidated + */ + public void setValidated(boolean isValidated) { + this.isValidated = isValidated; + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/ui/form/Validator.java b/docs-android/app/src/main/java/com/sismics/docs/ui/form/Validator.java new file mode 100644 index 00000000..9404c5d7 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/ui/form/Validator.java @@ -0,0 +1,124 @@ +package com.sismics.docs.ui.form; + +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.EditText; + +import com.sismics.docs.listener.CallbackListener; +import com.sismics.docs.ui.form.validator.ValidatorType; + +import java.util.HashMap; +import java.util.Map; + +/** + * Utility for form validation. + * + * @author bgamard + */ +public class Validator { + + /** + * List of validable elements. + */ + private Map validables = new HashMap(); + + /** + * Callback when the validation of one element has changed. + */ + private CallbackListener onValidationChanged; + + /** + * True if the validator show validation errors. + */ + private boolean showErrors; + + /** + * Constructor. + * + * @param showErrors True to display validation errors + */ + public Validator(boolean showErrors) { + this.showErrors = showErrors; + } + + /** + * Setter of onValidationChanged. + * @param onValidationChanged onValidationChanged + */ + public void setOnValidationChanged(CallbackListener onValidationChanged) { + this.onValidationChanged = onValidationChanged; + onValidationChanged.onComplete(); + } + + /** + * Add a validable element. + * + * @param editText Edit text + * @param validatorTypes Validators + */ + public void addValidable(final Context context, final EditText editText, final ValidatorType...validatorTypes) { + final Validable validable = new Validable(); + validables.put(editText, validable); + + editText.addTextChangedListener(new TextWatcher() { + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // NOP + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // NOP + } + + @Override + public void afterTextChanged(Editable s) { + validable.setValidated(true); + for (ValidatorType validatorType : validatorTypes) { + if (!validatorType.validate(s.toString())) { + if (showErrors) { + editText.setError(validatorType.getErrorMessage(context)); + } + validable.setValidated(false); + break; + } + } + + if (validable.isValidated()) { + editText.setError(null); + } + + if (onValidationChanged != null) { + onValidationChanged.onComplete(); + } + } + }); + } + + /** + * Returns true if the element is validated. + * + * @param view View + * @return True if the element is validated + */ + public boolean isValidated(View view) { + return validables.get(view).isValidated(); + } + + /** + * Returns true if all elements are validated. + * + * @return True if all elements are validated + */ + public boolean isValidated() { + for (Validable validable : validables.values()) { + if (!validable.isValidated()) { + return false; + } + } + return true; + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Alphanumeric.java b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Alphanumeric.java new file mode 100644 index 00000000..d0d54d26 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Alphanumeric.java @@ -0,0 +1,28 @@ +package com.sismics.docs.ui.form.validator; + +import android.content.Context; + +import com.sismics.docs.R; + +import java.util.regex.Pattern; + +/** + * Alphanumeric validator. + * + * @author bgamard + */ +public class Alphanumeric implements ValidatorType { + + private static Pattern ALPHANUMERIC_PATTERN = Pattern.compile("[a-zA-Z0-9_]+"); + + @Override + public boolean validate(String text) { + return ALPHANUMERIC_PATTERN.matcher(text).matches(); + } + + @Override + public String getErrorMessage(Context context) { + return context.getString(R.string.validate_error_alphanumeric); + } + +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Email.java b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Email.java new file mode 100644 index 00000000..a39d4c19 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Email.java @@ -0,0 +1,30 @@ +package com.sismics.docs.ui.form.validator; + +import android.content.Context; + +import com.sismics.docs.R; + +import java.util.regex.Pattern; + +/** + * Email validator. + * + * @author bgamard + */ +public class Email implements ValidatorType { + + /** + * Pattern de validation. + */ + private static Pattern EMAIL_PATTERN = Pattern.compile(".+@.+\\..+"); + + @Override + public boolean validate(String text) { + return EMAIL_PATTERN.matcher(text).matches(); + } + + @Override + public String getErrorMessage(Context context) { + return context.getResources().getString(R.string.validate_error_email); + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Length.java b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Length.java new file mode 100644 index 00000000..4283c1d2 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Length.java @@ -0,0 +1,52 @@ +package com.sismics.docs.ui.form.validator; + +import android.content.Context; + +import com.sismics.docs.R; + +/** + * Text length validator. + * + * @author bgamard + */ +public class Length implements ValidatorType { + + /** + * Minimum length. + */ + private int minLength = 0; + + /** + * Maximum length. + */ + private int maxLength = 0; + + /** + * True if the last validation error was about a string too short. + */ + private boolean tooShort; + + /** + * Constructor. + * @param minLength Minimum length + * @param maxLength Maximum length + */ + public Length(int minLength, int maxLength) { + this.minLength = minLength; + this.maxLength = maxLength; + } + + @Override + public boolean validate(String text) { + tooShort = text.trim().length() < minLength; + return text.trim().length() >= minLength && text.trim().length() <= maxLength; + } + + @Override + public String getErrorMessage(Context context) { + if (tooShort) { + return context.getResources().getString(R.string.validate_error_length_min, minLength); + } + return context.getResources().getString(R.string.validate_error_length_max, maxLength); + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Required.java b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Required.java new file mode 100644 index 00000000..111a49e9 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/Required.java @@ -0,0 +1,24 @@ +package com.sismics.docs.ui.form.validator; + +import android.content.Context; + +import com.sismics.docs.R; + +/** + * Text presence validator. + * + * @author bgamard + */ +public class Required implements ValidatorType { + + @Override + public boolean validate(String text) { + return text.trim().length() != 0; + } + + @Override + public String getErrorMessage(Context context) { + return context.getString(R.string.validate_error_required); + } + +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/ValidatorType.java b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/ValidatorType.java new file mode 100644 index 00000000..50acc3b2 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/ui/form/validator/ValidatorType.java @@ -0,0 +1,25 @@ +package com.sismics.docs.ui.form.validator; + +import android.content.Context; + +/** + * Interface for validation types. + * + * @author bgamard + */ +public interface ValidatorType { + + /** + * Returns true if the validator is validated. + * @param text + * @return + */ + public boolean validate(String text); + + /** + * Returns an error message. + * @param context + * @return + */ + public String getErrorMessage(Context context); +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/util/ApplicationUtil.java b/docs-android/app/src/main/java/com/sismics/docs/util/ApplicationUtil.java new file mode 100644 index 00000000..fba41d56 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/util/ApplicationUtil.java @@ -0,0 +1,43 @@ +package com.sismics.docs.util; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; + +/** + * Utility class on general application data. + * + * @author bgamard + */ +public class ApplicationUtil { + + /** + * Returns version name. + * + * @param context Context + * @return Nom de la version + */ + public static String getVersionName(Context context) { + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return packageInfo.versionName; + } catch (NameNotFoundException e) { + return ""; + } + } + + /** + * Returns version number. + * + * @param context Context + * @return Numéro de version + */ + public static int getVersionCode(Context context) { + try { + PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return packageInfo.versionCode; + } catch (NameNotFoundException e) { + return 0; + } + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/util/DialogUtil.java b/docs-android/app/src/main/java/com/sismics/docs/util/DialogUtil.java new file mode 100644 index 00000000..271977c6 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/util/DialogUtil.java @@ -0,0 +1,40 @@ +package com.sismics.docs.util; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; + +import com.sismics.docs.R; + +/** + * Utility class for dialogs. + * + * @author bgamard + */ +public class DialogUtil { + + /** + * Create a dialog with an OK button. + * + * @param activity Context activity + * @param title Dialog title + * @param message Dialog message + */ + public static void showOkDialog(Activity activity, int title, int message) { + if (activity == null || activity.isFinishing()) { + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + + builder.setTitle(title) + .setMessage(message) + .setCancelable(true) + .setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) { + dialog.dismiss(); + } + }).create().show(); + } +} diff --git a/docs-android/app/src/main/java/com/sismics/docs/util/PreferenceUtil.java b/docs-android/app/src/main/java/com/sismics/docs/util/PreferenceUtil.java new file mode 100644 index 00000000..23a2fd54 --- /dev/null +++ b/docs-android/app/src/main/java/com/sismics/docs/util/PreferenceUtil.java @@ -0,0 +1,162 @@ +package com.sismics.docs.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.preference.PreferenceManager; + +import com.loopj.android.http.PersistentCookieStore; + +import org.apache.http.cookie.Cookie; +import org.json.JSONObject; + +import java.util.List; + +/** + * Utility class on preferences. + * + * @author bgamard + */ +public class PreferenceUtil { + + public static final String PREF_CACHED_USER_INFO_JSON = "pref_cachedUserInfoJson"; + public static final String PREF_SERVER_URL = "pref_ServerUrl"; + + /** + * Returns a preference of boolean type. + * @param context Context + * @param key Shared preference key + * @return Shared preference value + */ + public static boolean getBooleanPreference(Context context, String key, boolean defaultValue) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + return sharedPreferences.getBoolean(key, defaultValue); + } + + /** + * Returns a preference of string type. + * @param context Context + * @param key Shared preference key + * @return Shared preference value + */ + public static String getStringPreference(Context context, String key) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + return sharedPreferences.getString(key, null); + } + + /** + * Returns a preference of integer type. + * @param context Context + * @param key Shared preference key + * @return Shared preference value + */ + public static int getIntegerPreference(Context context, String key, int defaultValue) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + try { + String pref = sharedPreferences.getString(key, ""); + try { + return Integer.parseInt(pref); + } catch (NumberFormatException e) { + return defaultValue; + } + } catch (ClassCastException e) { + return sharedPreferences.getInt(key, defaultValue); + } + + } + + /** + * Update JSON cache. + * @param context Context + * @param key Shared preference key + * @param json JSON data + */ + public static void setCachedJson(Context context, String key, JSONObject json) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + sharedPreferences.edit().putString(key, json != null ? json.toString() : null).commit(); + } + + /** + * Returns a JSON cache. + * @param context Context + * @param key Shared preference key + * @return JSON data + */ + public static JSONObject getCachedJson(Context context, String key) { + try { + return new JSONObject(getStringPreference(context, key)); + } catch (Exception e) { + // The cache is not parsable, clean this up + setCachedJson(context, key, null); + return null; + } + } + + /** + * Update server URL. + * @param context Context + */ + public static void setServerUrl(Context context, String serverUrl) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + sharedPreferences.edit().putString(PREF_SERVER_URL, serverUrl).commit(); + } + + /** + * Empty user caches. + * @param context Context + */ + public static void resetUserCache(Context context) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + Editor editor = sharedPreferences.edit(); + editor.putString(PREF_CACHED_USER_INFO_JSON, null); + editor.commit(); + } + + /** + * Returns auth token cookie from shared preferences. + * @return Auth token + */ + public static String getAuthToken(Context context) { + PersistentCookieStore cookieStore = new PersistentCookieStore(context); + List cookieList = cookieStore.getCookies(); + for (Cookie cookie : cookieList) { + if (cookie.getName().equals("auth_token")) { + return cookie.getValue(); + } + } + + return null; + } + + /** + * Returns cleaned server URL. + * @param context Context + * @return Server URL + */ + public static String getServerUrl(Context context) { + String serverUrl = getStringPreference(context, PREF_SERVER_URL); + if (serverUrl == null) { + return null; + } + + // Trim + serverUrl = serverUrl.trim(); + + if (!serverUrl.startsWith("http")) { + // Try to add http + serverUrl = "http://" + serverUrl; + } + + if (serverUrl.endsWith("/")) { + // Delete last / + serverUrl = serverUrl.substring(0, serverUrl.length() - 1); + } + + // Remove /api + if (serverUrl.endsWith("/api")) { + serverUrl = serverUrl.substring(0, serverUrl.length() - 4); + } + + return serverUrl; + } +} diff --git a/docs-android/app/src/main/res/drawable-hdpi/drawer_shadow.9.png b/docs-android/app/src/main/res/drawable-hdpi/drawer_shadow.9.png new file mode 100644 index 00000000..224cc4ff Binary files /dev/null and b/docs-android/app/src/main/res/drawable-hdpi/drawer_shadow.9.png differ diff --git a/docs-android/app/src/main/res/drawable-hdpi/ic_drawer.png b/docs-android/app/src/main/res/drawable-hdpi/ic_drawer.png new file mode 100644 index 00000000..ff7b1def Binary files /dev/null and b/docs-android/app/src/main/res/drawable-hdpi/ic_drawer.png differ diff --git a/docs-android/app/src/main/res/drawable-hdpi/list_background_holo.9.png b/docs-android/app/src/main/res/drawable-hdpi/list_background_holo.9.png new file mode 100644 index 00000000..694d6a4d Binary files /dev/null and b/docs-android/app/src/main/res/drawable-hdpi/list_background_holo.9.png differ diff --git a/docs-android/app/src/main/res/drawable-mdpi/ic_launcher.png b/docs-android/app/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index 359047df..00000000 Binary files a/docs-android/app/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/docs-android/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png b/docs-android/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png new file mode 100644 index 00000000..fa3d853e Binary files /dev/null and b/docs-android/app/src/main/res/drawable-xhdpi/drawer_shadow.9.png differ diff --git a/docs-android/app/src/main/res/drawable-xhdpi/ic_drawer.png b/docs-android/app/src/main/res/drawable-xhdpi/ic_drawer.png new file mode 100644 index 00000000..b9bc3d70 Binary files /dev/null and b/docs-android/app/src/main/res/drawable-xhdpi/ic_drawer.png differ diff --git a/docs-android/app/src/main/res/drawable-xhdpi/list_background_holo.9.png b/docs-android/app/src/main/res/drawable-xhdpi/list_background_holo.9.png new file mode 100644 index 00000000..9d4d5829 Binary files /dev/null and b/docs-android/app/src/main/res/drawable-xhdpi/list_background_holo.9.png differ diff --git a/docs-android/app/src/main/res/layout/activity_doc_detail.xml b/docs-android/app/src/main/res/layout/activity_doc_detail.xml deleted file mode 100644 index 758ecf6b..00000000 --- a/docs-android/app/src/main/res/layout/activity_doc_detail.xml +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/docs-android/app/src/main/res/layout/activity_doc_list.xml b/docs-android/app/src/main/res/layout/activity_doc_list.xml deleted file mode 100644 index bca7ceff..00000000 --- a/docs-android/app/src/main/res/layout/activity_doc_list.xml +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/docs-android/app/src/main/res/layout/activity_doc_twopane.xml b/docs-android/app/src/main/res/layout/activity_doc_twopane.xml deleted file mode 100644 index 2c9296cf..00000000 --- a/docs-android/app/src/main/res/layout/activity_doc_twopane.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - diff --git a/docs-android/app/src/main/res/layout/fragment_doc_detail.xml b/docs-android/app/src/main/res/layout/fragment_doc_detail.xml deleted file mode 100644 index 190266b7..00000000 --- a/docs-android/app/src/main/res/layout/fragment_doc_detail.xml +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/docs-android/app/src/main/res/layout/login_activity.xml b/docs-android/app/src/main/res/layout/login_activity.xml new file mode 100644 index 00000000..6c04d955 --- /dev/null +++ b/docs-android/app/src/main/res/layout/login_activity.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +