diff --git a/mobile/AndroidManifest.xml b/mobile/AndroidManifest.xml index 942e160b..d7e54c57 100644 --- a/mobile/AndroidManifest.xml +++ b/mobile/AndroidManifest.xml @@ -8,9 +8,11 @@ android:minSdkVersion="14" android:targetSdkVersion="17" /> - + + + + diff --git a/mobile/res/values/strings.xml b/mobile/res/values/strings.xml index ac731228..5639c5a6 100644 --- a/mobile/res/values/strings.xml +++ b/mobile/res/values/strings.xml @@ -55,4 +55,6 @@ Faction Info Debug + Choose account to login + Login failed. \ No newline at end of file diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_DeviceAccountLogin.java b/mobile/src/com/cradle/iitc_mobile/IITC_DeviceAccountLogin.java new file mode 100644 index 00000000..ba0ce3d1 --- /dev/null +++ b/mobile/src/com/cradle/iitc_mobile/IITC_DeviceAccountLogin.java @@ -0,0 +1,189 @@ +package com.cradle.iitc_mobile; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerCallback; +import android.accounts.AccountManagerFuture; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.BaseAdapter; +import android.widget.TextView; +import android.widget.Toast; + +/** + * this class manages automatic login using the Google account stored on the device + */ +public class IITC_DeviceAccountLogin implements AccountManagerCallback { + /** + * Adapter to show available accounts in a ListView. Accounts are read from mAccounts + */ + private class AccountAdapter extends BaseAdapter { + @Override + public int getCount() { + return mAccounts.length; + } + + @Override + public Account getItem(int position) { + return mAccounts[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = mActivity.getLayoutInflater(); + View v = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); + + TextView tv = (TextView) v.findViewById(android.R.id.text1); + tv.setText(mAccounts[position].name); + + return tv; + } + } + + private Account mAccount; + private AccountAdapter mAccountAdapter; + private AccountManager mAccountManager; + private Account[] mAccounts; + private IITC_Mobile mActivity; + private String mAuthToken; + private AlertDialog mProgressbar; + private WebView mWebView; + + /** + * This listener is invoked when an item in the account list is selected. (It is also used when the 'cancel' button + * is clicked, (in which case `index` is <0) + */ + private DialogInterface.OnClickListener onClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int index) { + if (index >= 0 && index < mAccounts.length) { + mAccount = mAccounts[index]; + startAuthentication(); + } + dialog.cancel(); + } + }; + + public IITC_DeviceAccountLogin(IITC_Mobile activity, WebView webView, WebViewClient webViewClient) { + mActivity = activity; + mWebView = webView; + mAccountManager = AccountManager.get(activity); + mAccountAdapter = new AccountAdapter(); + + // dialog that serves as a progress bar overlay + mProgressbar = new AlertDialog.Builder(mActivity) + .setCancelable(false) + .setView(mActivity.getLayoutInflater().inflate(R.layout.dialog_progressbar, null)) + .create(); + } + + /** + * display all available accounts to the user + */ + private void displayAccountList() { + AlertDialog.Builder builder = new AlertDialog.Builder(mActivity) + .setTitle(R.string.choose_account_to_login) + .setSingleChoiceItems(mAccountAdapter, 0, onClickListener) + .setNegativeButton(android.R.string.cancel, onClickListener); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + + /** + * called when something failed. Shows a toast message. Classic login is still available + */ + private void onLoginFailed() { + Toast.makeText(mActivity, R.string.login_failed, Toast.LENGTH_SHORT).show(); + } + + /** + * called to start authenticating using AccountManager. + * + * After a token is created, AccountManager will call the run() method. + */ + private void startAuthentication() { + mProgressbar.show(); + + mAccountManager.getAuthToken(mAccount, mAuthToken, null, mActivity, this, null); + } + + /** + * called by IITC_Mobile when the authentication activity has finished. + */ + public void onActivityResult(int resultCode, Intent data) { + if (resultCode == Activity.RESULT_OK) + // authentication activity succeeded, request token again + startAuthentication(); + else + onLoginFailed(); + } + + /** + * called by AccountManager + */ + @Override + public void run(AccountManagerFuture value) { + mProgressbar.hide(); + + try { + Intent launch = (Intent) value.getResult().get(AccountManager.KEY_INTENT); + if (launch != null) { + // There is a reason we need to start the given activity if we want an authentication token. + // (Could be user confirmation or something else. Whatever, we have to start it) + // IITC_Mobile will call it using startActivityForResult + mActivity.startLoginActivity(launch); + return; + } + + String result = value.getResult().getString(AccountManager.KEY_AUTHTOKEN); + if (result != null) { + // authentication succeded, we can load the given url, which will redirect back to the intel map + mWebView.loadUrl(result); + mActivity.loginSucceded(); + } else { + onLoginFailed(); + } + } catch (Exception e) { + onLoginFailed(); + } + } + + /** + * start authentication + * + * if we already have a username (e.g. because the existing login has timed out), we can directly start + * authentication if an account with that username is found. + */ + public void startLogin(String realm, String accountName, String args) { + mAccounts = mAccountManager.getAccountsByType(realm); + mAccountAdapter.notifyDataSetChanged(); + mAuthToken = "weblogin:" + args; + + if (mAccounts.length == 0) { + return; + } + + for (Account account : mAccounts) { + if (account.name.equals(accountName)) { + mAccountManager.getAuthToken(account, mAuthToken, null, mActivity, this, null); + return; + } + } + + displayAccountList(); + } +} diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java b/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java index 9af53094..e937c686 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java @@ -2,7 +2,13 @@ package com.cradle.iitc_mobile; import java.io.IOException; -import com.cradle.iitc_mobile.R; +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.content.res.Configuration; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; @@ -13,21 +19,18 @@ import android.os.Bundle; import android.os.Handler; import android.os.StrictMode; import android.preference.PreferenceManager; -import android.app.ActionBar; -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.res.Configuration; import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import android.view.Window; import android.view.WindowManager; +import android.webkit.WebView; import android.widget.Toast; public class IITC_Mobile extends Activity { + private static final int REQUEST_LOGIN = 1; + private IITC_WebView iitc_view; private boolean back_button_pressed = false; private OnSharedPreferenceChangeListener listener; @@ -38,11 +41,15 @@ public class IITC_Mobile extends Activity { private boolean fullscreen_mode = false; private boolean fullscreen_actionbar = false; private ActionBar actionBar; + private IITC_DeviceAccountLogin mLogin; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // enable progress bar above action bar + requestWindowFeature(Window.FEATURE_PROGRESS); + // TODO build an async task for url.openStream() in IITC_WebViewClient StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder() .permitAll().build(); @@ -354,4 +361,42 @@ public class IITC_Mobile extends Activity { public IITC_WebView getWebView() { return this.iitc_view; } + + /** + * It can occur that in order to authenticate, an external activity has to be launched. (This could for example be a + * confirmation dialog.) + */ + public void startLoginActivity(Intent launch) { + startActivityForResult(launch, REQUEST_LOGIN); // REQUEST_LOGIN is to recognize the result + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_LOGIN : + // authentication activity has returned. mLogin will continue authentication + mLogin.onActivityResult(resultCode, data); + break; + + default : + super.onActivityResult(requestCode, resultCode, data); + } + } + + /** + * called by IITC_WebViewClient when the Google login form is opened. + */ + public void onReceivedLoginRequest(IITC_WebViewClient client, WebView view, + String realm, String account, String args) { + mLogin = new IITC_DeviceAccountLogin(this, view, client); + mLogin.startLogin(realm, account, args); + } + + /** + * called after successful login + */ + public void loginSucceded() { + // garbage collection + mLogin = null; + } } diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_WebView.java b/mobile/src/com/cradle/iitc_mobile/IITC_WebView.java index 866d1f38..b3a752a3 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_WebView.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_WebView.java @@ -2,6 +2,7 @@ package com.cradle.iitc_mobile; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.net.ConnectivityManager; @@ -39,14 +40,29 @@ public class IITC_WebView extends WebView { this.js_interface = new IITC_JSInterface(c); this.addJavascriptInterface(js_interface, "android"); - // our webchromeclient should share geolocation with the iitc script - // allow access by default this.setWebChromeClient(new WebChromeClient() { + /** + * our webchromeclient should share geolocation with the iitc script + * + * allow access by default + */ @Override public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { callback.invoke(origin, true, false); } + + /** + * display progress bar in activity + */ + @Override + public void onProgressChanged(WebView view, int newProgress) { + super.onProgressChanged(view, newProgress); + + // maximum for newProgress is 100 + // maximum for setProgress is 10,000 + ((Activity) getContext()).setProgress(newProgress * 100); + } }); webclient = new IITC_WebViewClient(c); diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_WebViewClient.java b/mobile/src/com/cradle/iitc_mobile/IITC_WebViewClient.java index f606d5ac..6f75ab6f 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_WebViewClient.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_WebViewClient.java @@ -133,6 +133,14 @@ public class IITC_WebViewClient extends WebViewClient { handler.proceed(); }; + /** + * this method is called automatically when the Google login form is opened. + */ + @Override + public void onReceivedLoginRequest(WebView view, String realm, String account, String args) { + ((IITC_Mobile) context).onReceivedLoginRequest(this, view, realm, account, args); + } + // plugins should be loaded after the main script is injected @Override public void onPageFinished(WebView view, String url) {