/*
* WikiFragment.java
* Copyright (C) 2015 Nicholas Killewald
*
* This file is distributed under the terms of the BSD license.
* The source package should have a LICENSE file at the toplevel.
*/
package net.exclaimindustries.geohashdroid.fragments;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Editable;
import android.text.SpannableString;
import android.text.TextWatcher;
import android.text.style.UnderlineSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import net.exclaimindustries.geohashdroid.R;
import net.exclaimindustries.geohashdroid.services.WikiService;
import net.exclaimindustries.geohashdroid.util.GHDConstants;
import net.exclaimindustries.geohashdroid.util.Graticule;
import net.exclaimindustries.geohashdroid.util.Info;
import net.exclaimindustries.geohashdroid.util.UnitConverter;
import net.exclaimindustries.geohashdroid.wiki.WikiUtils;
import net.exclaimindustries.tools.BitmapTools;
import net.exclaimindustries.tools.LocationUtil;
import java.text.DateFormat;
import java.util.Calendar;
/**
* <code>WikiFragment</code> does double duty, handling what both of <code>WikiPictureEditor</code>
* and <code>WikiMessageEditor</code> used to do. Well, most of it. Honestly,
* most of that's been dumped into {@link WikiService}, but the interface part
* here can handle either pictures or messages.
*/
public class WikiFragment extends CentralMapExtraFragment {
private static final String PICTURE_URI = "pictureUri";
private static final int GET_PICTURE = 1;
private View mAnonWarning;
private ImageButton mGalleryButton;
private CheckBox mPictureCheckbox;
private CheckBox mIncludeLocationCheckbox;
private TextView mLocationView;
private TextView mDistanceView;
private EditText mMessage;
private Button mPostButton;
private TextView mHeader;
private Location mLastLocation;
private Uri mPictureUri;
private SharedPreferences.OnSharedPreferenceChangeListener mPrefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
// Huh, we register for ALL changes, not just for a few prefs. May
// as well narrow it down...
if(key.equals(GHDConstants.PREF_WIKI_USER) || key.equals(GHDConstants.PREF_WIKI_PASS)) {
checkAnonStatus();
}
}
};
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.wiki, container, false);
// Views!
mAnonWarning = layout.findViewById(R.id.wiki_anon_warning);
mPictureCheckbox = (CheckBox)layout.findViewById(R.id.wiki_check_include_picture);
mIncludeLocationCheckbox = (CheckBox)layout.findViewById(R.id.wiki_check_include_location);
mGalleryButton = (ImageButton)layout.findViewById(R.id.wiki_thumbnail);
mPostButton = (Button)layout.findViewById(R.id.wiki_post_button);
mMessage = (EditText)layout.findViewById(R.id.wiki_message);
mLocationView = (TextView)layout.findViewById(R.id.wiki_current_location);
mDistanceView = (TextView)layout.findViewById(R.id.wiki_distance);
mHeader = (TextView)layout.findViewById(R.id.wiki_header);
// The picture checkbox determines if the other boxes are visible or
// not.
mPictureCheckbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
resolvePictureControlVisibility();
}
});
// The gallery button needs to fire off to the gallery. Or Photos. Or
// whatever's listening for this intent.
mGalleryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Apparently there's been some... changes. Changes in Kitkat
// or the like.
Intent i;
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
i = new Intent(Intent.ACTION_GET_CONTENT);
i.setType("image/*");
} else {
i = new Intent(Intent.ACTION_OPEN_DOCUMENT);
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setType("image/*");
}
startActivityForResult(i, GET_PICTURE);
}
});
// Any time the user edits the text, we also check to re-enable the post
// button.
mMessage.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) { }
@Override
public void afterTextChanged(Editable s) {
// Ah, there we go.
resolvePostButtonEnabledness();
}
});
// The header goes to the current wiki page.
mHeader.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(mInfo != null) {
Intent i = new Intent();
i.setAction(Intent.ACTION_VIEW);
i.setData(Uri.parse(WikiUtils.getWikiBaseViewUrl() + WikiUtils.getWikiPageName(mInfo)));
startActivity(i);
}
}
});
// Here's the main event.
mPostButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dispatchPost();
}
});
// Make sure the header gets set here, too.
applyHeader();
// If we had a leftover Uri, apply that as well.
if(savedInstanceState != null) {
Uri pic = savedInstanceState.getParcelable(PICTURE_URI);
if(pic != null) setImageUri(pic);
} else {
// setImageUri will call resolvePostButtonEnabledness, but since we
// don't want to pass a null to the former, we'll call the latter if
// we got a null.
resolvePostButtonEnabledness();
}
updateCheckbox();
return layout;
}
@Override
public void onResume() {
super.onResume();
// We do the anon checks on resume, since it's possible that the user
// came back from preferences and the anon states have changed.
checkAnonStatus();
// Plus, resubscribe for those changes.
PreferenceManager.getDefaultSharedPreferences(getActivity()).registerOnSharedPreferenceChangeListener(mPrefListener);
// Update the location, too. This also makes the location fields
// invisible if permissions aren't granted yet. permissionsDenied()
// will cover if they suddenly became available.
updateLocation();
}
@Override
public void onPause() {
// Stop listening for changes. We'll redo anon checks on resume anyway.
PreferenceManager.getDefaultSharedPreferences(getActivity()).unregisterOnSharedPreferenceChangeListener(mPrefListener);
super.onPause();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// We've also got a picture URI to deal with.
outState.putParcelable(PICTURE_URI, mPictureUri);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode) {
case GET_PICTURE: {
if(data != null) {
// Picture in! We need to stash the URL away and make a
// thumbnail out of it, if we can!
Uri uri = data.getData();
if(uri == null)
return;
setImageUri(uri);
}
}
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
private void setImageUri(@NonNull Uri uri) {
// Grab a new Bitmap. We'll toss this into the button.
int dimen = getResources().getDimensionPixelSize(R.dimen.wiki_nominal_icon_size);
final Bitmap thumbnail = BitmapTools
.createRatioPreservedDownscaledBitmapFromUri(
getActivity(),
uri,
dimen,
dimen,
true
);
// Good! Was it null?
if(thumbnail == null) {
// NO! WRONG! BAD!
Toast.makeText(getActivity(), R.string.wiki_generic_image_error, Toast.LENGTH_LONG).show();
return;
}
// With bitmap in hand...
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mGalleryButton.setImageBitmap(thumbnail);
}
});
// And remember it for posting later. Done!
mPictureUri = uri;
resolvePostButtonEnabledness();
}
@Override
public void setInfo(Info info) {
super.setInfo(info);
applyHeader();
}
private void checkAnonStatus() {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// A user is anonymous if they either have no username or no
// password (the wiki doesn't allow passwordless users, which
// would just be silly anyway).
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
String username = prefs.getString(GHDConstants.PREF_WIKI_USER, "");
String password = prefs.getString(GHDConstants.PREF_WIKI_PASS, "");
if(username.isEmpty() || password.isEmpty()) {
// If anything isn't defined, we can't set a picture. Also,
// uncheck the picture checkbox just to make sure.
mPictureCheckbox.setChecked(false);
mPictureCheckbox.setVisibility(View.GONE);
mGalleryButton.setVisibility(View.GONE);
mAnonWarning.setVisibility(View.VISIBLE);
} else {
// Now, we can't just turn everything back on without
// checking. But we CAN get rid of the anon warning and
// bring back the picture checkbox.
mAnonWarning.setVisibility(View.GONE);
mPictureCheckbox.setVisibility(View.VISIBLE);
}
// Now, make sure everything else is up to date, including the
// text on the post button. This will do some redundant checks
// in the case of hiding things, but meh.
resolvePictureControlVisibility();
}
});
}
private void resolvePictureControlVisibility() {
// One checkbox to rule them all!
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if(mPictureCheckbox.isChecked()) {
mGalleryButton.setVisibility(View.VISIBLE);
// Oh, and update a few strings, too.
mPostButton.setText(R.string.wiki_dialog_submit_picture);
mIncludeLocationCheckbox.setText(R.string.wiki_dialog_stamp_image);
mMessage.setHint(R.string.hint_caption);
} else {
mGalleryButton.setVisibility(View.GONE);
mPostButton.setText(R.string.wiki_dialog_submit_message);
mIncludeLocationCheckbox.setText(R.string.wiki_dialog_append_coordinates);
mMessage.setHint(R.string.hint_message);
}
// This also changes the post button's enabledness.
resolvePostButtonEnabledness();
}
});
}
private void resolvePostButtonEnabledness() {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// We can make a few booleans here just so the eventual call to
// setEnabled is easier to read.
boolean isInPictureMode = mPictureCheckbox.isChecked();
boolean hasPicture = (mPictureUri != null);
boolean hasMessage = !(mMessage.getText().toString().isEmpty());
// So, to review, the button is enabled ONLY if there's a
// message and, if we're in picture mode, there's a picture to
// go with it.
mPostButton.setEnabled(hasMessage && (hasPicture || !isInPictureMode));
}
});
}
private void applyHeader() {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if(mInfo == null) {
mHeader.setText("");
} else {
// Make sure it's underlined so it at least LOOKS like a
// thing someone might click.
Graticule g = mInfo.getGraticule();
SpannableString text = new SpannableString(getString(R.string.wiki_dialog_header,
DateFormat.getDateInstance(DateFormat.MEDIUM).format(mInfo.getCalendar().getTime()),
(g == null
? getString(R.string.globalhash_label)
: g.getLatitudeString(false) + " " + g.getLongitudeString(false))));
text.setSpan(new UnderlineSpan(), 0, text.length(), 0);
mHeader.setText(text);
}
}
});
}
private void updateLocation() {
// If we're not ready yet (or if this isn't a phone layout), don't
// bother.
if(mLocationView == null || mDistanceView == null || mInfo == null) return;
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// Easy enough, this is just the current location data.
if(mPermissionsDenied) {
mLocationView.setVisibility(View.INVISIBLE);
mDistanceView.setVisibility(View.INVISIBLE);
} else {
mLocationView.setVisibility(View.VISIBLE);
mDistanceView.setVisibility(View.VISIBLE);
}
if(mLastLocation == null) {
// Or not, if there's no location.
mLocationView.setText(R.string.standby_title);
mDistanceView.setText(R.string.standby_title);
} else {
mLocationView.setText(UnitConverter.makeFullCoordinateString(getActivity(), mLastLocation, false, UnitConverter.OUTPUT_SHORT));
mDistanceView.setText(UnitConverter.makeDistanceString(getActivity(), UnitConverter.DISTANCE_FORMAT_SHORT, mLastLocation.distanceTo(mInfo.getFinalLocation())));
}
}
});
}
private void dispatchPost() {
// Time for fun!
boolean includeLocation = !mPermissionsDenied && mIncludeLocationCheckbox.isChecked();
boolean includePicture = mPictureCheckbox.isChecked();
// So. If we didn't have an Info yet, we're hosed.
if(mInfo == null) {
Toast.makeText(getActivity(), R.string.error_no_data_to_wiki, Toast.LENGTH_LONG).show();
return;
}
// If there's no message, we're hosed.
if(mMessage.getText().toString().isEmpty()) {
Toast.makeText(getActivity(), R.string.error_no_message, Toast.LENGTH_LONG).show();
return;
}
// If this is a picture post but there's no picture, we're hosed.
if(includePicture && mPictureUri == null) {
Toast.makeText(getActivity(), R.string.error_no_picture, Toast.LENGTH_LONG).show();
return;
}
// Otherwise, it's time to send!
String message = mMessage.getText().toString();
Location loc = mLastLocation;
if(!LocationUtil.isLocationNewEnough(loc)) loc = null;
Intent i = new Intent(getActivity(), WikiService.class);
i.putExtra(WikiService.EXTRA_INFO, mInfo);
i.putExtra(WikiService.EXTRA_TIMESTAMP, Calendar.getInstance());
i.putExtra(WikiService.EXTRA_MESSAGE, message);
i.putExtra(WikiService.EXTRA_LOCATION, loc);
i.putExtra(WikiService.EXTRA_INCLUDE_LOCATION, includeLocation);
if(includePicture)
i.putExtra(WikiService.EXTRA_IMAGE, mPictureUri);
// And away it goes!
getActivity().startService(i);
// Post complete! We're done here!
if(mCloseListener != null)
mCloseListener.extraFragmentClosing(this);
}
@NonNull
@Override
public FragmentType getType() {
return FragmentType.WIKI;
}
@Override
public void onLocationChanged(Location location) {
// We'll get this in either from CentralMap or WikiActivity. In either
// case, we act the same way.
mLastLocation = location;
updateLocation();
}
@Override
public void permissionsDenied(boolean denied) {
// This comes in from ExpeditionMode if permissions are denied/granted
// or from WikiActivity during onResume if permissions are granted some
// other way (WikiActivity won't ask for permission; it'll just assume
// the current permission state holds).
mPermissionsDenied = denied;
updateLocation();
// Also, remove the Append Location box if permissions were denied.
updateCheckbox();
}
private void updateCheckbox() {
mIncludeLocationCheckbox.setVisibility(mPermissionsDenied ? View.GONE : View.VISIBLE);
}
}