2014-01-26 16:06:10 +01:00

810 lines
28 KiB
Java

package com.cradle.iitc_mobile;
import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DownloadManager;
import android.app.SearchManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.res.Configuration;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.NfcEvent;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.webkit.CookieManager;
import android.webkit.WebView;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.SearchView;
import android.widget.TextView;
import android.widget.Toast;
import com.cradle.iitc_mobile.IITC_NavigationHelper.Pane;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Stack;
import java.util.Vector;
public class IITC_Mobile extends Activity
implements OnSharedPreferenceChangeListener, NfcAdapter.CreateNdefMessageCallback {
private static final String mIntelUrl = "https://www.ingress.com/intel";
private SharedPreferences mSharedPrefs;
private IITC_FileManager mFileManager;
private IITC_WebView mIitcWebView;
private IITC_UserLocation mUserLocation;
private IITC_NavigationHelper mNavigationHelper;
private IITC_MapSettings mMapSettings;
private IITC_DeviceAccountLogin mLogin;
private final Vector<ResponseHandler> mResponseHandlers = new Vector<ResponseHandler>();
private boolean mDesktopMode = false;
private boolean mAdvancedMenu = false;
private MenuItem mSearchMenuItem;
private View mImageLoading;
private ListView mLvDebug;
private View mViewDebug;
private ImageButton mBtnToggleMap;
private EditText mEditCommand;
private boolean mDebugging = false;
private boolean mReloadNeeded = false;
private boolean mIsLoading = true;
private boolean mShowMapInDebug = false;
private final Stack<String> mDialogStack = new Stack<String>();
private String mPermalink = null;
// Used for custom back stack handling
private final Stack<Pane> mBackStack = new Stack<IITC_NavigationHelper.Pane>();
private Pane mCurrentPane = Pane.MAP;
private boolean mBackButtonPressed = false;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
((IITC_Mobile) context).installIitcUpdate();
}
};
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// enable progress bar above action bar
requestWindowFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
mImageLoading = findViewById(R.id.imageLoading);
mIitcWebView = (IITC_WebView) findViewById(R.id.iitc_webview);
mLvDebug = (ListView) findViewById(R.id.lvDebug);
mViewDebug = findViewById(R.id.viewDebug);
mBtnToggleMap = (ImageButton) findViewById(R.id.btnToggleMapVisibility);
mEditCommand = (EditText) findViewById(R.id.editCommand);
mEditCommand.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(final TextView v, final int actionId, final KeyEvent event) {
if (EditorInfo.IME_ACTION_GO == actionId) {
onBtnRunCodeClick(v);
final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
return true;
}
return false;
}
});
mLvDebug.setAdapter(new IITC_LogAdapter(this));
// do something if user changed something in the settings
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
mSharedPrefs.registerOnSharedPreferenceChangeListener(this);
// enable/disable mDesktopMode mode on menu create and url load
mDesktopMode = mSharedPrefs.getBoolean("pref_force_desktop", false);
// enable/disable advance menu
mAdvancedMenu = mSharedPrefs.getBoolean("pref_advanced_menu", false);
// get fullscreen status from settings
mIitcWebView.updateFullscreenStatus();
mFileManager = new IITC_FileManager(this);
mUserLocation = new IITC_UserLocation(this);
mUserLocation.setLocationMode(Integer.parseInt(mSharedPrefs.getString("pref_user_location_mode", "0")));
// pass ActionBar to helper because we deprecated getActionBar
mNavigationHelper = new IITC_NavigationHelper(this, super.getActionBar());
mMapSettings = new IITC_MapSettings(this);
// Clear the back stack
mBackStack.clear();
// receive downloadManagers downloadComplete intent
// afterwards install iitc update
registerReceiver(mBroadcastReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
final NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
if (nfc != null) nfc.setNdefPushMessageCallback(this, this);
handleIntent(getIntent(), true);
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String key) {
if (key.equals("pref_force_desktop")) {
mDesktopMode = sharedPreferences.getBoolean("pref_force_desktop", false);
mNavigationHelper.onPrefChanged();
} else if (key.equals("pref_user_location_mode")) {
final int mode = Integer.parseInt(mSharedPrefs.getString("pref_user_location_mode", "0"));
if (mUserLocation.setLocationMode(mode))
mReloadNeeded = true;
return;
} else if (key.equals("pref_fullscreen")) {
mIitcWebView.updateFullscreenStatus();
mNavigationHelper.onPrefChanged();
return;
} else if (key.equals("pref_advanced_menu")) {
mAdvancedMenu = sharedPreferences.getBoolean("pref_advanced_menu", false);
mNavigationHelper.setDebugMode(mAdvancedMenu);
invalidateOptionsMenu();
// no reload needed
return;
} else if (key.equals("pref_fake_user_agent")) {
mIitcWebView.setUserAgent();
} else if (key.equals("pref_press_twice_to_exit")
|| key.equals("pref_share_selected_tab")
|| key.equals("pref_messages")
|| key.equals("pref_external_storage")) {
// no reload needed
return;
}
mReloadNeeded = true;
}
@Override
protected void onNewIntent(final Intent intent) {
setIntent(intent);
handleIntent(intent, false);
}
private void handleIntent(final Intent intent, final boolean onCreate) {
// load new iitc web view with ingress intel page
final String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action) || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
final Uri uri = intent.getData();
Log.d("intent received url: " + uri.toString());
if (uri.getScheme().equals("http") || uri.getScheme().equals("https")) {
if (uri.getHost() != null
&& (uri.getHost().equals("ingress.com") || uri.getHost().endsWith(".ingress.com"))) {
Log.d("loading url...");
loadUrl(uri.toString());
return;
}
}
if (uri.getScheme().equals("geo")) {
try {
handleGeoUri(uri);
return;
} catch (final URISyntaxException e) {
Log.w(e);
new AlertDialog.Builder(this)
.setTitle(R.string.intent_error)
.setMessage(e.getReason())
.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
}
})
.create()
.show();
}
}
}
if (Intent.ACTION_SEARCH.equals(action)) {
String query = intent.getStringExtra(SearchManager.QUERY);
query = query.replace("'", "''");
final SearchView searchView =
(SearchView) mSearchMenuItem.getActionView();
searchView.setQuery(query, false);
searchView.clearFocus();
switchToPane(Pane.MAP);
mIitcWebView.loadUrl("javascript:search('" + query + "');");
return;
}
if (onCreate) {
loadUrl(mIntelUrl);
}
}
private void handleGeoUri(final Uri uri) throws URISyntaxException {
final String[] parts = uri.getSchemeSpecificPart().split("\\?", 2);
Double lat, lon;
Integer z = null;
// parts[0] may contain an 'uncertainty' parameter, delimited by a semicolon
final String[] pos = parts[0].split(";", 2)[0].split(",", 2);
if (pos.length != 2) throw new URISyntaxException(uri.toString(), "URI does not contain a valid position");
try {
lat = Double.valueOf(pos[0]);
lon = Double.valueOf(pos[1]);
} catch (final NumberFormatException e) {
final URISyntaxException use = new URISyntaxException(uri.toString(), "position could not be parsed");
use.initCause(e);
throw use;
}
if (parts.length > 1) { // query string present
// search for z=
for (final String param : parts[1].split("&")) {
if (param.startsWith("z=")) {
try {
z = Integer.valueOf(param.substring(2));
} catch (final NumberFormatException e) {
final URISyntaxException use = new URISyntaxException(
uri.toString(), "could not parse zoom level");
use.initCause(e);
throw use;
}
break;
}
}
}
String url = "http://www.ingress.com/intel?ll=" + lat + "," + lon;
if (z != null) {
url += "&z=" + z;
}
loadUrl(url);
}
@Override
protected void onStart() {
super.onStart();
if (mReloadNeeded) {
Log.d("preference had changed...reload needed");
reloadIITC();
} else {
// iitc is not fully booted...timer will be reset by the script itself
if (findViewById(R.id.imageLoading).getVisibility() == View.GONE) {
// enough idle...let's do some work
Log.d("resuming...reset idleTimer");
mIitcWebView.loadUrl("javascript: window.idleReset();");
}
}
mUserLocation.onStart();
}
@Override
protected void onResume() {
super.onResume();
mIitcWebView.resumeTimers();
mIitcWebView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mIitcWebView.pauseTimers();
mIitcWebView.onPause();
}
@Override
protected void onStop() {
super.onStop();
Log.d("stopping iitcm");
mIitcWebView.loadUrl("javascript: window.idleSet();");
mUserLocation.onStop();
}
@Override
protected void onDestroy() {
unregisterReceiver(mBroadcastReceiver);
super.onDestroy();
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mNavigationHelper.onConfigurationChanged(newConfig);
Log.d("configuration changed...restoring...reset idleTimer");
mIitcWebView.loadUrl("javascript: window.idleTime = 0");
mIitcWebView.loadUrl("javascript: window.renderUpdateStatus()");
}
@Override
protected void onPostCreate(final Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
mNavigationHelper.onPostCreate(savedInstanceState);
}
// we want a self defined behavior for the back button
@Override
public void onBackPressed() {
// exit fullscreen mode if it is enabled and action bar is disabled or the back stack is empty
if (mIitcWebView.isInFullscreen() && mBackStack.isEmpty()) {
mIitcWebView.toggleFullscreen();
return;
}
// close drawer if opened
if (mNavigationHelper.isDrawerOpened()) {
mNavigationHelper.closeDrawers();
return;
}
// kill all open iitc dialogs
if (!mDialogStack.isEmpty()) {
final String id = mDialogStack.pop();
mIitcWebView.loadUrl("javascript: " +
"var selector = $(window.DIALOGS['" + id + "']); " +
"selector.dialog('close'); " +
"selector.remove();");
return;
}
// Pop last item from backstack and pretend the relevant menu item was clicked
if (!mBackStack.isEmpty()) {
backStackPop();
mBackButtonPressed = true;
return;
}
if (mBackButtonPressed || !mSharedPrefs.getBoolean("pref_press_twice_to_exit", false)) {
super.onBackPressed();
} else {
mBackButtonPressed = true;
Toast.makeText(this, "Press twice to exit", Toast.LENGTH_SHORT).show();
// reset back button after 2 seconds
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mBackButtonPressed = false;
}
}, 2000);
}
}
public void backStackPop() {
// shouldn't be called when back stack is empty
// catch wrong usage
if (mBackStack.isEmpty()) {
mBackStack.push(Pane.MAP);
}
final Pane pane = mBackStack.pop();
switchToPane(pane);
}
public void setCurrentPane(final Pane pane) {
// ensure no double adds
if (pane == mCurrentPane) return;
// map pane is top-lvl. clear stack.
if (pane == Pane.MAP) {
mBackStack.clear();
}
// don't push current pane to backstack if this method was called via back button
else if (!mBackButtonPressed) mBackStack.push(mCurrentPane);
mBackButtonPressed = false;
mCurrentPane = pane;
mNavigationHelper.switchTo(pane);
}
public void switchToPane(final Pane pane) {
mIitcWebView.loadUrl("javascript: window.show('" + pane.name + "');");
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
// Get the SearchView and set the searchable configuration
final SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
mSearchMenuItem = menu.findItem(R.id.menu_search);
final SearchView searchView =
(SearchView) mSearchMenuItem.getActionView();
// Assumes current activity is the searchable activity
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setIconifiedByDefault(false); // Do not iconify the widget; expand it by default
return true;
}
@Override
public boolean onPrepareOptionsMenu(final Menu menu) {
boolean visible = false;
if (mNavigationHelper != null)
visible = !mNavigationHelper.isDrawerOpened();
for (int i = 0; i < menu.size(); i++) {
final MenuItem item = menu.getItem(i);
switch (item.getItemId()) {
case R.id.action_settings:
item.setVisible(true);
break;
case R.id.menu_clear_cookies:
item.setVisible(mAdvancedMenu && visible);
break;
case R.id.locate:
item.setVisible(visible);
item.setIcon(mUserLocation.isFollowing()
? R.drawable.ic_action_location_follow
: R.drawable.ic_action_location_found);
break;
case R.id.menu_debug:
item.setVisible(mAdvancedMenu && visible);
item.setChecked(mDebugging);
break;
default:
item.setVisible(visible);
}
}
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (mNavigationHelper.onOptionsItemSelected(item)) return true;
// Handle item selection
final int itemId = item.getItemId();
switch (itemId) {
case android.R.id.home:
switchToPane(Pane.MAP);
return true;
case R.id.reload_button:
reloadIITC();
return true;
case R.id.toggle_fullscreen:
mIitcWebView.toggleFullscreen();
return true;
case R.id.layer_chooser:
mNavigationHelper.openRightDrawer();
return true;
case R.id.locate: // get the users current location and focus it on map
switchToPane(Pane.MAP);
if (mUserLocation.hasCurrentLocation()) {
// if gps location is displayed we can use a better location without any costs
mUserLocation.locate();
} else {
// get location from network by default
mIitcWebView.loadUrl("javascript: window.map.locate({setView : true});");
}
return true;
case R.id.action_settings: // start settings activity
final Intent intent = new Intent(this, IITC_PreferenceActivity.class);
try {
intent.putExtra("iitc_version", mFileManager.getIITCVersion());
} catch (final IOException e) {
Log.w(e);
return true;
}
startActivity(intent);
return true;
case R.id.menu_clear_cookies:
final CookieManager cm = CookieManager.getInstance();
cm.removeAllCookie();
return true;
case R.id.menu_debug:
mDebugging = !mDebugging;
updateViews();
invalidateOptionsMenu();
// TODO remove debugging stuff from JS?
return true;
default:
return false;
}
}
@Override
public File getCacheDir() {
return getApplicationContext().getCacheDir();
}
public void reloadIITC() {
mNavigationHelper.reset();
mMapSettings.reset();
mUserLocation.reset();
mIitcWebView.getWebViewClient().reset();
mBackStack.clear();
// iitc starts on map after reload
mCurrentPane = Pane.MAP;
loadUrl(mIntelUrl);
mReloadNeeded = false;
}
// vp=f enables mDesktopMode mode...vp=m is the default mobile view
private String addUrlParam(final String url) {
if (mDesktopMode) {
return (url + "?vp=f");
} else {
return (url + "?vp=m");
}
}
// inject the iitc-script and load the intel url
// plugins are injected onPageFinished
public void loadUrl(String url) {
setLoadingState(true);
url = addUrlParam(url);
mIitcWebView.loadUrl(url);
}
public IITC_WebView getWebView() {
return mIitcWebView;
}
public void startActivityForResult(final Intent launch, final ResponseHandler handler) {
int index = mResponseHandlers.indexOf(handler);
if (index == -1) {
mResponseHandlers.add(handler);
index = mResponseHandlers.indexOf(handler);
}
startActivityForResult(launch, RESULT_FIRST_USER + index);
}
public void deleteResponseHandler(final ResponseHandler handler) {
final int index = mResponseHandlers.indexOf(handler);
if (index != -1) {
// set value to null to enable garbage collection, but don't remove it to keep indexes
mResponseHandlers.set(index, null);
}
}
@Override
protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
final int index = requestCode - RESULT_FIRST_USER;
try {
final ResponseHandler handler = mResponseHandlers.get(index);
handler.onActivityResult(resultCode, data);
} catch (final ArrayIndexOutOfBoundsException e) {
super.onActivityResult(requestCode, resultCode, data);
}
}
/**
* called by IITC_WebViewClient when the Google login form is opened.
*/
public void onReceivedLoginRequest(final IITC_WebViewClient client, final WebView view, final String realm,
final String account, final String args) {
mLogin = new IITC_DeviceAccountLogin(this, view, client);
mLogin.startLogin(realm, account, args);
}
/**
* called after successful login
*/
public void loginSucceeded() {
// garbage collection
mLogin = null;
setLoadingState(true);
}
// remove dialog and add it back again
// to ensure it is the last element of the list
// focused dialogs should be closed first
public void setFocusedDialog(final String id) {
Log.d("Dialog " + id + " focused");
mDialogStack.remove(id);
mDialogStack.push(id);
}
// called by the javascript interface
public void dialogOpened(final String id, final boolean open) {
if (open) {
Log.d("Dialog " + id + " added");
mDialogStack.push(id);
} else {
Log.d("Dialog " + id + " closed");
mDialogStack.remove(id);
}
}
public void setLoadingState(final boolean isLoading) {
mIsLoading = isLoading;
mNavigationHelper.onLoadingStateChanged();
updateViews();
}
private void updateViews() {
if (!mDebugging) {
mViewDebug.setVisibility(View.GONE);
mLvDebug.setVisibility(View.GONE);
if (mIsLoading && !mSharedPrefs.getBoolean("pref_disable_splash", false)) {
mIitcWebView.setVisibility(View.GONE);
mImageLoading.setVisibility(View.VISIBLE);
} else {
mIitcWebView.setVisibility(View.VISIBLE);
mImageLoading.setVisibility(View.GONE);
}
} else {
// if the debug container is invisible (and we are about to show it), select the text box
final boolean select = mViewDebug.getVisibility() != View.VISIBLE;
mImageLoading.setVisibility(View.GONE); // never show splash screen while debugging
mViewDebug.setVisibility(View.VISIBLE);
if (select) {
mEditCommand.requestFocus();
mEditCommand.selectAll();
}
if (mShowMapInDebug) {
mBtnToggleMap.setImageResource(R.drawable.ic_action_view_as_list);
mIitcWebView.setVisibility(View.VISIBLE);
mLvDebug.setVisibility(View.GONE);
} else {
mBtnToggleMap.setImageResource(R.drawable.ic_action_map);
mIitcWebView.setVisibility(View.GONE);
mLvDebug.setVisibility(View.VISIBLE);
}
}
}
public void onBtnRunCodeClick(final View v) {
final String code = mEditCommand.getText().toString();
final JSONObject obj = new JSONObject();
try {
obj.put("code", code);
} catch (final JSONException e) {
Log.w(e);
return;
}
// throwing an exception will be reported by WebView
final String js = "(function(obj){var result;" +
"console.log('>>> ' + obj.code);" +
"try{result=eval(obj.code);}catch(e){if(e.stack) console.error(e.stack);throw e;}" +
"if(result!==undefined) console.log(result.toString());" +
"})(" + obj.toString() + ");";
mIitcWebView.loadJS(js);
}
/**
* onClick handler for R.id.btnToggleMapVisibility, assigned in activity_main.xml
*/
public void onToggleMapVisibility(final View v)
{
mShowMapInDebug = !mShowMapInDebug;
updateViews();
}
private void deleteUpdateFile() {
final File file = new File(getExternalFilesDir(null).toString() + "/iitcUpdate.apk");
if (file != null) file.delete();
}
public void updateIitc(final String url) {
final DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setDescription(getString(R.string.download_description));
request.setTitle("IITCm Update");
request.allowScanningByMediaScanner();
final Uri fileUri = Uri.parse("file://" + getExternalFilesDir(null).toString() + "/iitcUpdate.apk");
request.setDestinationUri(fileUri);
// remove old update file...we don't want to spam the external storage
deleteUpdateFile();
// get download service and enqueue file
final DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
manager.enqueue(request);
}
private void installIitcUpdate() {
final String iitcUpdatePath = getExternalFilesDir(null).toString() + "/iitcUpdate.apk";
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(new File(iitcUpdatePath)), "application/vnd.android.package-archive");
startActivity(intent);
// finish app, because otherwise it gets killed on update
finish();
}
public boolean isLoading() {
return mIsLoading;
}
/**
* @see getNavigationHelper()
* @deprecated ActionBar related stuff should be handled by IITC_NavigationHelper
*/
@Deprecated
@Override
public ActionBar getActionBar() {
return super.getActionBar();
}
public IITC_NavigationHelper getNavigationHelper() {
return mNavigationHelper;
}
public IITC_MapSettings getMapSettings() {
return mMapSettings;
}
public IITC_FileManager getFileManager() {
return mFileManager;
}
public SharedPreferences getPrefs() {
return mSharedPrefs;
}
public IITC_UserLocation getUserLocation() {
return mUserLocation;
}
public interface ResponseHandler {
void onActivityResult(int resultCode, Intent data);
}
public void setPermalink(final String href) {
mPermalink = href;
}
@Override
public NdefMessage createNdefMessage(final NfcEvent event) {
NdefRecord[] records;
if (mPermalink == null) { // no permalink yet, just provide AAR
records = new NdefRecord[] {
NdefRecord.createApplicationRecord(getPackageName())
};
} else {
records = new NdefRecord[] {
NdefRecord.createUri(mPermalink),
NdefRecord.createApplicationRecord(getPackageName())
};
}
return new NdefMessage(records);
}
}