package net.wigle.wigleandroid;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.res.Resources;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.Fragment;
import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.text.method.PasswordTransformationMethod;
import android.text.method.SingleLineTransformationMethod;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import net.wigle.wigleandroid.background.ApiDownloader;
import net.wigle.wigleandroid.background.DownloadHandler;
import static net.wigle.wigleandroid.UserStatsFragment.MSG_USER_DONE;
/**
* configure settings
*/
public final class SettingsFragment extends Fragment implements DialogListener {
private static final int MENU_RETURN = 12;
private static final int MENU_ERROR_REPORT = 13;
private static final int DONATE_DIALOG=112;
private static final int ANONYMOUS_DIALOG=113;
private static final int DEAUTHORIZE_DIALOG=114;
public boolean allowRefresh = false;
/** convenience, just get the darn new string */
public static abstract class SetWatcher implements TextWatcher {
@Override
public void afterTextChanged( final Editable s ) {}
@Override
public void beforeTextChanged( final CharSequence s, final int start, final int count, final int after ) {}
@Override
public void onTextChanged( final CharSequence s, final int start, final int before, final int count ) {
onTextChanged( s.toString() );
}
public abstract void onTextChanged( String s );
}
/** Called when the activity is first created. */
@Override
public void onCreate( final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// set language
MainActivity.setLocale(getActivity());
}
@SuppressLint("SetTextI18n")
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.settings, container, false);
// force media volume controls
getActivity().setVolumeControlStream(AudioManager.STREAM_MUSIC);
// don't let the textbox have focus to start with, so we don't see a keyboard right away
final LinearLayout linearLayout = (LinearLayout) view.findViewById(R.id.linearlayout);
linearLayout.setFocusableInTouchMode(true);
linearLayout.requestFocus();
updateView(view);
return view;
}
@SuppressLint("SetTextI18n")
@Override
public void handleDialog(final int dialogId) {
final SharedPreferences prefs = getActivity().getSharedPreferences(ListFragment.SHARED_PREFS, 0);
final Editor editor = prefs.edit();
final View view = getView();
switch (dialogId) {
case DONATE_DIALOG: {
editor.putBoolean(ListFragment.PREF_DONATE, true);
editor.apply();
if (view != null) {
final CheckBox donate = (CheckBox) view.findViewById(R.id.donate);
donate.setChecked(true);
}
// poof
eraseDonate();
break;
}
case ANONYMOUS_DIALOG: {
// turn anonymous
editor.putBoolean( ListFragment.PREF_BE_ANONYMOUS, true );
editor.remove(ListFragment.PREF_USERNAME);
editor.remove(ListFragment.PREF_PASSWORD);
editor.remove(ListFragment.PREF_AUTHNAME);
editor.remove(ListFragment.PREF_TOKEN);
editor.apply();
if (view != null) {
this.updateView(view);
}
break;
}
case DEAUTHORIZE_DIALOG: {
editor.remove(ListFragment.PREF_AUTHNAME);
editor.remove(ListFragment.PREF_TOKEN);
editor.apply();
if (view != null) {
this.updateView(view);
}
break;
}
default:
MainActivity.warn("Settings unhandled dialogId: " + dialogId);
}
}
@Override
public void onResume() {
MainActivity.info("resume settings.");
final SharedPreferences prefs = getActivity().getSharedPreferences(ListFragment.SHARED_PREFS, 0);
// donate
final boolean isDonate = prefs.getBoolean(ListFragment.PREF_DONATE, false);
if ( isDonate ) {
eraseDonate();
}
super.onResume();
MainActivity.info("Resume with allow: "+allowRefresh);
if (allowRefresh) {
allowRefresh = false;
final View view = getView();
updateView(view);
//ALIBI: what doesn't work here:
//does not successfully reload
//getFragmentManager().beginTransaction().replace(this.container.getId(),this).commit();
// WTF: actually re-pauses and resumes.
//getFragmentManager().beginTransaction().detach(this).attach(this).commit();
}
getActivity().setTitle(R.string.settings_app_name);
}
@Override
public void onPause() {
super.onPause();
MainActivity.info("Pause; setting allowRefresh");
allowRefresh = true;
}
private void updateView(final View view) {
final SharedPreferences prefs = getActivity().getSharedPreferences(ListFragment.SHARED_PREFS, 0);
final Editor editor = prefs.edit();
// donate
final CheckBox donate = (CheckBox) view.findViewById(R.id.donate);
final boolean isDonate = prefs.getBoolean( ListFragment.PREF_DONATE, false);
donate.setChecked( isDonate );
if ( isDonate ) {
eraseDonate();
}
donate.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged( final CompoundButton buttonView, final boolean isChecked ) {
if ( isChecked == prefs.getBoolean( ListFragment.PREF_DONATE, false) ) {
// this would cause no change, bail
return;
}
if ( isChecked ) {
// turn off until confirmed
buttonView.setChecked( false );
// confirm
MainActivity.createConfirmation( getActivity(),
getString(R.string.donate_question) + "\n\n"
+ getString(R.string.donate_explain),
MainActivity.SETTINGS_TAB_POS, DONATE_DIALOG);
}
else {
editor.putBoolean( ListFragment.PREF_DONATE, false);
editor.apply();
}
}
});
final String authUser = prefs.getString(ListFragment.PREF_AUTHNAME,"");
final EditText user = (EditText) view.findViewById(R.id.edit_username);
final TextView authUserDisplay = (TextView) view.findViewById(R.id.show_authuser);
final TextView authUserLabel = (TextView) view.findViewById(R.id.show_authuser_label);
final EditText passEdit = (EditText) view.findViewById(R.id.edit_password);
final TextView passEditLabel = (TextView) view.findViewById(R.id.edit_password_label);
final CheckBox showPass = (CheckBox) view.findViewById(R.id.showpassword);
final String authToken = prefs.getString(ListFragment.PREF_TOKEN, "");
final Button deauthButton = (Button) view.findViewById(R.id.deauthorize_client);
final Button authButton = (Button) view.findViewById(R.id.authorize_client);
if (!authUser.isEmpty()) {
authUserDisplay.setText(authUser);
authUserDisplay.setVisibility(View.VISIBLE);
authUserLabel.setVisibility(View.VISIBLE);
if (!authToken.isEmpty()) {
deauthButton.setVisibility(View.VISIBLE);
deauthButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
MainActivity.createConfirmation( getActivity(),
getString(R.string.deauthorize_confirm),
MainActivity.SETTINGS_TAB_POS, DEAUTHORIZE_DIALOG );
}
});
authButton.setVisibility(View.GONE);
passEdit.setVisibility(View.GONE);
passEditLabel.setVisibility(View.GONE);
showPass.setVisibility(View.GONE);
user.setEnabled(false);
} else {
user.setEnabled(true);
}
} else {
user.setEnabled(true);
authUserDisplay.setVisibility(View.GONE);
authUserLabel.setVisibility(View.GONE);
deauthButton.setVisibility(View.GONE);
passEdit.setVisibility(View.VISIBLE);
passEditLabel.setVisibility(View.VISIBLE);
showPass.setVisibility(View.VISIBLE);
authButton.setVisibility(View.VISIBLE);
final Handler handler = new UserDownloadHandler(view, getActivity().getPackageName(),
getResources(), this);
final UserStatsFragment.UserDownloadApiListener apiListener =
new UserStatsFragment.UserDownloadApiListener(handler);
authButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
final ApiDownloader task = new ApiDownloader(getActivity(), ListFragment.lameStatic.dbHelper,
"user-stats-cache.json", MainActivity.USER_STATS_URL, false, true, true,
ApiDownloader.REQUEST_GET,
apiListener);
try {
task.startDownload(SettingsFragment.this);
} catch (WiGLEAuthException waex) {
MainActivity.info("User authentication failed");
}
}
});
}
// anonymous
final CheckBox beAnonymous = (CheckBox) view.findViewById(R.id.be_anonymous);
final boolean isAnonymous = prefs.getBoolean( ListFragment.PREF_BE_ANONYMOUS, false);
if ( isAnonymous ) {
user.setEnabled( false );
passEdit.setEnabled( false );
}
beAnonymous.setChecked( isAnonymous );
beAnonymous.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged( final CompoundButton buttonView, final boolean isChecked ) {
if ( isChecked == prefs.getBoolean(ListFragment.PREF_BE_ANONYMOUS, false) ) {
// this would cause no change, bail
return;
}
if ( isChecked ) {
// turn off until confirmed
buttonView.setChecked( false );
// confirm
MainActivity.createConfirmation( getActivity(),
getString(R.string.anonymous_confirm), MainActivity.SETTINGS_TAB_POS,
ANONYMOUS_DIALOG );
} else {
// unset anonymous
user.setEnabled(true);
passEdit.setEnabled(true);
editor.putBoolean( ListFragment.PREF_BE_ANONYMOUS, false );
editor.apply();
// might have to remove or show register link
updateRegister(view);
}
}
});
// register link
final TextView register = (TextView) view.findViewById(R.id.register);
final String registerString = getString(R.string.register);
final String activateString = getString(R.string.activate);
String registerBlurb = "<a href='https://wigle.net/register'>" + registerString +
"</a> @WiGLE.net";
// ALIBI: vision APIs started in 4.2.2; JB2 4.3 = 18 is safe. 17 might work...
// but we're only supporting qr in v23+ via the uses-permission-sdk-23 tag -rksh
if (Build.VERSION.SDK_INT >= 23) {
registerBlurb += " or <a href='net.wigle.wigleandroid://activate'>" + activateString +
"</a>";
}
try {
if (Build.VERSION.SDK_INT >= 24) {
register.setText(Html.fromHtml(registerBlurb,
Html.FROM_HTML_MODE_LEGACY));
} else {
register.setText(Html.fromHtml(registerBlurb));
}
} catch (Exception ex) {
register.setText(registerString + " @WiGLE.net");
}
register.setMovementMethod(LinkMovementMethod.getInstance());
updateRegister(view);
user.setText( prefs.getString( ListFragment.PREF_USERNAME, "" ) );
user.addTextChangedListener( new SetWatcher() {
@Override
public void onTextChanged( final String s ) {
credentialsUpdate(ListFragment.PREF_USERNAME, editor, prefs, s);
// might have to remove or show register link
updateRegister(view);
}
});
final CheckBox showPassword = (CheckBox) view.findViewById(R.id.showpassword);
showPassword.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged( final CompoundButton buttonView, final boolean isChecked ) {
if ( isChecked ) {
passEdit.setTransformationMethod(SingleLineTransformationMethod.getInstance());
}
else {
passEdit.setTransformationMethod(PasswordTransformationMethod.getInstance());
}
}
});
passEdit.setText( prefs.getString( ListFragment.PREF_PASSWORD, "" ) );
passEdit.addTextChangedListener( new SetWatcher() {
@Override
public void onTextChanged( final String s ) {
credentialsUpdate(ListFragment.PREF_PASSWORD, editor, prefs, s);
}
});
final Button button = (Button) view.findViewById(R.id.speech_button);
button.setOnClickListener( new OnClickListener() {
@Override
public void onClick( final View view ) {
final Intent errorReportIntent = new Intent( getActivity(), SpeechActivity.class );
SettingsFragment.this.startActivity( errorReportIntent );
}
});
// period spinners
doScanSpinner( R.id.periodstill_spinner, ListFragment.PREF_SCAN_PERIOD_STILL,
MainActivity.SCAN_STILL_DEFAULT, getString(R.string.nonstop), view );
doScanSpinner( R.id.period_spinner, ListFragment.PREF_SCAN_PERIOD,
MainActivity.SCAN_DEFAULT, getString(R.string.nonstop), view );
doScanSpinner( R.id.periodfast_spinner, ListFragment.PREF_SCAN_PERIOD_FAST,
MainActivity.SCAN_FAST_DEFAULT, getString(R.string.nonstop), view );
doScanSpinner( R.id.gps_spinner, ListFragment.GPS_SCAN_PERIOD,
MainActivity.LOCATION_UPDATE_INTERVAL, getString(R.string.setting_tie_wifi), view );
MainActivity.prefBackedCheckBox(this, view, R.id.edit_showcurrent, ListFragment.PREF_SHOW_CURRENT, true);
MainActivity.prefBackedCheckBox(this, view, R.id.use_metric, ListFragment.PREF_METRIC, false);
MainActivity.prefBackedCheckBox(this, view, R.id.found_sound, ListFragment.PREF_FOUND_SOUND, true);
MainActivity.prefBackedCheckBox(this, view, R.id.found_new_sound, ListFragment.PREF_FOUND_NEW_SOUND, true);
MainActivity.prefBackedCheckBox(this, view, R.id.circle_size_map, ListFragment.PREF_CIRCLE_SIZE_MAP, false);
MainActivity.prefBackedCheckBox(this, view, R.id.use_network_location, ListFragment.PREF_USE_NETWORK_LOC, false);
MainActivity.prefBackedCheckBox(this, view, R.id.disable_toast, ListFragment.PREF_DISABLE_TOAST, false);
final String[] languages = new String[]{ "", "en", "ar", "cs", "da", "de", "es", "fi", "fr", "fy",
"he", "hi", "hu", "it", "ja", "ko", "nl", "no", "pl", "pt", "pt-rBR", "ru", "sv", "tr", "zh" };
final String[] languageName = new String[]{ getString(R.string.auto), getString(R.string.language_en),
getString(R.string.language_ar), getString(R.string.language_cs), getString(R.string.language_da),
getString(R.string.language_de), getString(R.string.language_es), getString(R.string.language_fi),
getString(R.string.language_fr), getString(R.string.language_fy), getString(R.string.language_he),
getString(R.string.language_hi), getString(R.string.language_hu), getString(R.string.language_it),
getString(R.string.language_ja), getString(R.string.language_ko), getString(R.string.language_nl),
getString(R.string.language_no), getString(R.string.language_pl), getString(R.string.language_pt),
getString(R.string.language_pt_rBR), getString(R.string.language_ru), getString(R.string.language_sv),
getString(R.string.language_tr), getString(R.string.language_zh),
};
doSpinner( R.id.language_spinner, view, ListFragment.PREF_LANGUAGE, "", languages, languageName );
final String off = getString(R.string.off);
final String sec = " " + getString(R.string.sec);
final String min = " " + getString(R.string.min);
// battery kill spinner
final Long[] batteryPeriods = new Long[]{ 1L,2L,3L,4L,5L,10L,15L,20L,0L };
final String[] batteryName = new String[]{ "1 %","2 %","3 %","4 %","5 %","10 %","15 %","20 %",off };
doSpinner( R.id.battery_kill_spinner, view, ListFragment.PREF_BATTERY_KILL_PERCENT,
MainActivity.DEFAULT_BATTERY_KILL_PERCENT, batteryPeriods, batteryName );
// reset wifi spinner
final Long[] resetPeriods = new Long[]{ 15000L,30000L,60000L,90000L,120000L,300000L,600000L,0L };
final String[] resetName = new String[]{ "15" + sec, "30" + sec,"1" + min,"1.5" + min,
"2" + min,"5" + min,"10" + min,off };
doSpinner( R.id.reset_wifi_spinner, view, ListFragment.PREF_RESET_WIFI_PERIOD,
MainActivity.DEFAULT_RESET_WIFI_PERIOD, resetPeriods, resetName );
}
private void updateRegister(final View view) {
final SharedPreferences prefs = getActivity().getSharedPreferences(ListFragment.SHARED_PREFS, 0);
final String username = prefs.getString(ListFragment.PREF_USERNAME, "");
final boolean isAnonymous = prefs.getBoolean(ListFragment.PREF_BE_ANONYMOUS, false);
if (view != null) {
final TextView register = (TextView) view.findViewById(R.id.register);
//ALIBI: ActivateAcitivity.receiveDetections sets isAnonymous = false
if ("".equals(username) || isAnonymous) {
register.setEnabled(true);
register.setVisibility(View.VISIBLE);
} else {
// poof
register.setEnabled(false);
register.setVisibility(View.GONE);
}
}
}
/**
* The little dance we do when we update username or password, removing old creds/cache
* @param key the ListFragment key (u or p)
* @param editor prefs editor reference
* @param prefs preferences for checks
* @param newValue the new value for the username or pass
*/
public void credentialsUpdate(String key, Editor editor, SharedPreferences prefs, String newValue) {
//DEBUG: MainActivity.info(key + ": " + newValue.trim());
String currentValue = prefs.getString(key, "");
if (currentValue.equals(newValue.trim())) {
return;
}
if (newValue.trim().isEmpty()) {
//ALIBI: empty values should unset
editor.remove(key);
} else {
editor.putString(key, newValue.trim());
}
// ALIBI: if the u|p changes, force refetch token
editor.remove(ListFragment.PREF_AUTHNAME);
editor.remove(ListFragment.PREF_TOKEN);
editor.apply();
this.clearCachefiles();
}
/**
* clear cache files (i.e. on creds change)
*/
private void clearCachefiles() {
final File cacheDir = new File(MainActivity.getSDPath());
final File[] cacheFiles = cacheDir.listFiles(new FilenameFilter() {
@Override
public boolean accept( final File dir,
final String name ) {
return name.matches( ".*-cache\\.json" );
}
} );
if (null != cacheFiles) {
for (File cache: cacheFiles) {
//DEBUG: MainActivity.info("deleting: " + cache.getAbsolutePath());
boolean deleted = cache.delete();
if (!deleted) {
MainActivity.warn("failed to delete cache file: "+cache.getAbsolutePath());
}
}
}
}
private void eraseDonate() {
final View view = getView();
if (view != null) {
final CheckBox donate = (CheckBox) view.findViewById(R.id.donate);
donate.setEnabled(false);
donate.setVisibility(View.GONE);
}
}
private void doScanSpinner( final int id, final String pref, final long spinDefault,
final String zeroName, final View view ) {
final String ms = " " + getString(R.string.ms_short);
final String sec = " " + getString(R.string.sec);
final String min = " " + getString(R.string.min);
final Long[] periods = new Long[]{ 0L,50L,250L,500L,750L,1000L,1500L,2000L,3000L,4000L,5000L,10000L,30000L,60000L };
final String[] periodName = new String[]{ zeroName,"50" + ms,"250" + ms,"500" + ms,"750" + ms,
"1" + sec,"1.5" + sec,"2" + sec,
"3" + sec,"4" + sec,"5" + sec,"10" + sec,"30" + sec,"1" + min };
doSpinner(id, view, pref, spinDefault, periods, periodName);
}
private <V> void doSpinner(final int id, final View view, final String pref, final V spinDefault,
final V[] periods, final String[] periodName) {
doSpinner((Spinner)view.findViewById(id), pref, spinDefault, periods, periodName, getContext());
}
public static <V> void doSpinner( final Spinner spinner, final String pref, final V spinDefault, final V[] periods,
final String[] periodName, final Context context ) {
if ( periods.length != periodName.length ) {
throw new IllegalArgumentException("lengths don't match, periods: " + Arrays.toString(periods)
+ " periodName: " + Arrays.toString(periodName));
}
final SharedPreferences prefs = context.getSharedPreferences(ListFragment.SHARED_PREFS, 0);
final Editor editor = prefs.edit();
ArrayAdapter<String> adapter = new ArrayAdapter<>(
context, android.R.layout.simple_spinner_item);
Object period = null;
if ( periods instanceof Long[] ) {
period = prefs.getLong( pref, (Long) spinDefault );
}
else if ( periods instanceof String[] ) {
period = prefs.getString( pref, (String) spinDefault );
}
else {
MainActivity.error("unhandled object type array: " + Arrays.toString(periods) + " class: " + periods.getClass());
}
if (period == null) {
period = periods[0];
}
int periodIndex = 0;
for ( int i = 0; i < periods.length; i++ ) {
adapter.add( periodName[i] );
if ( period.equals(periods[i]) ) {
periodIndex = i;
}
}
adapter.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item );
spinner.setAdapter( adapter );
spinner.setSelection( periodIndex );
spinner.setOnItemSelectedListener( new OnItemSelectedListener() {
@Override
public void onItemSelected( final AdapterView<?> parent, final View v, final int position, final long id ) {
// set pref
final V period = periods[position];
MainActivity.info( pref + " setting scan period: " + period );
if ( period instanceof Long ) {
editor.putLong( pref, (Long) period );
}
else if ( period instanceof String ) {
editor.putString( pref, (String) period );
}
else {
MainActivity.error("unhandled object type: " + period + " class: " + period.getClass());
}
editor.apply();
if ( period instanceof String ) {
MainActivity.setLocale( context, context.getResources().getConfiguration() );
}
}
@Override
public void onNothingSelected( final AdapterView<?> arg0 ) {}
});
}
/* Creates the menu items */
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
MenuItem item = menu.add( 0, MENU_ERROR_REPORT, 0, getString(R.string.menu_error_report) );
item.setIcon( android.R.drawable.ic_menu_report_image );
item = menu.add(0, MENU_RETURN, 0, getString(R.string.menu_return));
item.setIcon( android.R.drawable.ic_media_previous );
}
/* Handles item selections */
@Override
public boolean onOptionsItemSelected( final MenuItem item ) {
switch ( item.getItemId() ) {
case MENU_RETURN:
final MainActivity mainActivity = MainActivity.getMainActivity(this);
if (mainActivity != null) mainActivity.selectFragment(MainActivity.LIST_TAB_POS);
return true;
case MENU_ERROR_REPORT:
final Intent errorReportIntent = new Intent( getActivity(), ErrorReportActivity.class );
this.startActivity( errorReportIntent );
break;
}
return false;
}
/**
* used for authentication - this seems really heavy
*/
private final static class UserDownloadHandler extends DownloadHandler {
private SettingsFragment fragment;
private UserDownloadHandler(final View view, final String packageName,
final Resources resources, SettingsFragment settingsFragment) {
super(view, null, packageName, resources);
fragment = settingsFragment;
}
@SuppressLint("SetTextI18n")
@Override
public void handleMessage(final Message msg) {
final Bundle bundle = msg.getData();
if (msg.what == MSG_USER_DONE) {
if (bundle.containsKey("error")) {
//ALIBI: not doing anything more here, since the toast will alert.
MainActivity.info("Settings auth unsuccessful");
} else {
MainActivity.info("Settings auth successful");
final SharedPreferences prefs = MainActivity.getMainActivity()
.getApplicationContext()
.getSharedPreferences(ListFragment.SHARED_PREFS, 0);
final Editor editor = prefs.edit();
editor.remove(ListFragment.PREF_PASSWORD);
editor.apply();
//TODO: order dependent -verify no risk of race condition here.
fragment.updateView(view);
}
}
}
}
}