/**
* Dialogs.java Created on 13 Jun 2013 Copyright 2013 Michele Bonazza
* <emmepuntobi@gmail.com>
*
* This file is part of WhatsHare.
*
* WhatsHare is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* Foobar is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* WhatsHare. If not, see <http://www.gnu.org/licenses/>.
*/
package it.mb.whatshare;
import it.mb.whatshare.MainActivity.PairedDevice;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.json.JSONException;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentActivity;
import android.text.InputType;
import android.text.SpannableStringBuilder;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
/**
* Dialogs used throughout the app.
*
* <p>
* The initial purpose of this class was to have a single point where to
* dynamically switch between fragments from the support package and fragments
* from the standard library. Anyway, because of the awesome design decision to
* force activities to extend {@link FragmentActivity} for fragments to work,
* it's impossible to switch between old-fashioned fragments and the new ones
* (you can't have different flavors of {@link MainActivity} in the manifest
* according to the API level).
*
* <p>
* If and when this app will drop support for API <11, all activities can
* stop to extend {@link FragmentActivity}, and all calls to
* {@link FragmentActivity#getSupportFragmentManager()} can be replaced with
* {@link Activity#getFragmentManager()}.
*
* @author Michele Bonazza
*/
@SuppressLint("ValidFragment")
public class Dialogs {
private static class DeviceNameChooserPrompt implements
DialogInterface.OnShowListener {
private final AlertDialog alertDialog;
private final EditText input;
private final MainActivity activity;
private final Callback<String> callback;
private DeviceNameChooserPrompt(AlertDialog alertDialog,
EditText input, MainActivity activity, Callback<String> callback) {
this.alertDialog = alertDialog;
this.input = input;
this.activity = activity;
this.callback = callback;
}
@Override
public void onShow(DialogInterface dialog) {
Button b = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
b.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
input.setError(null);
String deviceName = input.getText().toString();
if (!Pattern.matches(MainActivity.VALID_DEVICE_NAME,
deviceName)) {
if (deviceName.length() < 1) {
setError(input, R.string.at_least_one_char,
activity);
} else {
setError(input, R.string.wrong_char, activity);
}
} else {
callback.setParam(deviceName).run();
alertDialog.dismiss();
}
}
});
}
}
private static abstract class Callback<T> implements Runnable {
private T param;
/**
* Sets the parameter that the executor of the callback can use inside
* its {@link #run()} implementation.
*
* <p>
* This method <b>must always</b> be called <b>before</b> calling
* {@link #run()}
*
* @param arg
* the parameter to set
* @return a reference to this class, so calls can be chained
*/
public Callback<T> setParam(T arg) {
this.param = arg;
return this;
}
/**
* Returns the parameter that was set by whomever is calling
* {@link #run()} on this callback.
*
* @return the parameter that was set by the caller
*/
public T getParam() {
return param;
}
}
private static final String CUSTOM_TYPEFACE_PATH = "fonts/PTM55FT.ttf";
/**
* Shows a dialog informing the user that the QR code she's taken a picture
* of is not valid.
*
* @param activity
* the caller activity
*/
public static void onQRFail(final FragmentActivity activity) {
DialogFragment failDialog = new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
return getBuilder(activity)
.setMessage(R.string.qr_code_fail)
.setPositiveButton(R.string.qr_code_retry,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// do nothing
}
}).create();
}
};
failDialog.show(activity.getSupportFragmentManager(), "fail");
}
/**
* Shows a dialog that informs the user of the result of a pairing action,
* and saves the newly paired outbound device if the operation was
* successful.
*
* <p>
* Once the user taps on the OK button,
* {@link Activity#startActivity(Intent)} is called to get back to the
* {@link MainActivity}.
*
* @param device
* the outbound device to be paired
* @param activity
* the caller activity
*/
public static void onPairingOutbound(final PairedDevice device,
final FragmentActivity activity) {
new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = getBuilder(activity);
try {
builder.setMessage(getString(R.string.failed_pairing));
if (device != null) {
PairOutboundActivity.savePairing(device, activity);
builder.setMessage(getResources().getString(
R.string.successful_pairing, device.type));
}
} catch (IOException e) {
// TODO let user know
e.printStackTrace();
} catch (JSONException e) {
e.printStackTrace();
}
builder.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// back to main screen
startActivity(new Intent(activity,
MainActivity.class));
}
});
return builder.create();
}
}.show(activity.getSupportFragmentManager(), "code");
}
/**
* Shows a dialog informing the user about what went wrong while trying to
* registrate her device with GCM.
*
* @param errorCode
* the error message's resource ID within <tt>strings.xml</tt>
* @param activity
* the caller activity
* @param finishActivityOnOk
* whether the call comes from an activity with no UI that
* requires clicks on ok (or back) to call
* {@link Activity#finish()} on the caller activity
*/
public static void onRegistrationError(final int errorCode,
final FragmentActivity activity, final boolean finishActivityOnOk) {
new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder;
if (finishActivityOnOk) {
builder = getNoUiBuilder(activity);
} else {
builder = getBuilder(activity);
}
builder.setMessage(getString(R.string.gcm_registration_error,
getString(errorCode)));
builder.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// just hide dialog
}
});
Dialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(!finishActivityOnOk);
return dialog;
}
}.show(activity.getSupportFragmentManager(), "gcm_error");
}
/**
* Shows a dialog containing the code received by goo.gl if any, or an error
* message stating that the pairing operation failed.
*
* @param googl
* the pairing code retrieved by goo.gl
* @param activity
* the caller activity
*/
public static void onObtainPairingCode(final String googl,
final MainActivity activity) {
new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = getBuilder(activity);
if (googl != null) {
View layout = View.inflate(getContext(),
R.layout.pairing_code_dialog, null);
TextView message = (TextView) layout
.findViewById(R.id.pairingCode);
Typeface typeFace = Typeface.createFromAsset(
activity.getAssets(), CUSTOM_TYPEFACE_PATH);
message.setTypeface(typeFace);
message.setText(googl);
builder.setView(layout);
} else {
builder.setMessage(getResources().getString(
R.string.code_dialog_fail));
}
builder.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
if (googl != null) {
Resources res = getResources();
String howManyTotal = res
.getQuantityString(
R.plurals.added_device,
activity.getInboundDevicesCount(),
activity.getInboundDevicesCount());
Toast.makeText(
getActivity(),
res.getString(
R.string.device_paired,
howManyTotal),
Toast.LENGTH_LONG).show();
}
}
});
return builder.create();
}
}.show(activity.getSupportFragmentManager(), "resultCode");
}
/**
* Shows a dialog to get a name for the inbound device being paired and
* starts a new {@link CallGooGlInbound} action if the chosen name is valid.
*
* @param deviceType
* the model of the device being paired as suggested by the
* device itself
* @param sharedSecret
* the keys used when encrypting the message between devices
* @param activity
* the caller activity
*/
public static void promptForInboundName(final String deviceType,
final int[] sharedSecret, final MainActivity activity) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
DialogFragment prompt = new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = getBuilder(activity);
final EditText input = new EditText(getContext());
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(deviceType);
input.setSelection(deviceType.length());
// @formatter:off
builder.setTitle(R.string.device_name_chooser_title)
.setView(input)
.setPositiveButton(android.R.string.ok, null);
// @formatter:on
final AlertDialog alertDialog = builder.create();
alertDialog
.setOnShowListener(new DeviceNameChooserPrompt(
alertDialog, input, activity,
new Callback<String>() {
@Override
public void run() {
// @formatter:off
new CallGooGlInbound(activity,
getParam(),
deviceType)
.execute(sharedSecret);
((InputMethodManager) activity
.getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(
input.getWindowToken(),
0);
// @formatter:on
}
}));
return alertDialog;
}
};
prompt.show(activity.getSupportFragmentManager(), "chooseName");
}
});
}
/**
* Shows a prompt to the user to rename the argument <tt>device</tt>.
*
* @param device
* the device to be renamed
* @param activity
* the parent activity
*/
public static void promptForNewDeviceName(final PairedDevice device,
final MainActivity activity) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
DialogFragment prompt = new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = getBuilder(activity);
final EditText input = new EditText(getContext());
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(device.name);
input.setSelection(device.name.length());
// @formatter:off
builder.setTitle(R.string.device_name_chooser_title)
.setView(input)
.setPositiveButton(android.R.string.ok, null);
// @formatter:on
final AlertDialog alertDialog = builder.create();
alertDialog
.setOnShowListener(new DeviceNameChooserPrompt(
alertDialog, input, activity,
new Callback<String>() {
@Override
public void run() {
// @formatter:off
device.rename(getParam());
activity.onSelectedDeviceRenamed();
((InputMethodManager) activity
.getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(
input.getWindowToken(),
0);
// @formatter:on
}
}));
return alertDialog;
}
};
prompt.show(activity.getSupportFragmentManager(), "chooseName");
}
});
}
/**
* Sad workaround.
*
* See http://stackoverflow.com/a/7350315/1159164
*
* @param input
* the EditText to set the error to
* @param errorID
* the ID of the error in <tt>strings.xml</tt>
* @param activity
* the enclosing activity
*/
private static void setError(EditText input, int errorID, Activity activity) {
String errorMsg = activity.getResources().getString(errorID);
if (Build.VERSION.SDK_INT < 11) {
ForegroundColorSpan fgcspan = new ForegroundColorSpan(Color.BLACK);
SpannableStringBuilder ssbuilder = new SpannableStringBuilder(
errorMsg);
ssbuilder.setSpan(fgcspan, 0, errorMsg.length(), 0);
input.setError(ssbuilder);
} else {
input.setError(errorMsg);
}
}
/**
* Asks the user for confirmation of a delete outbound device operation.
*
* <p>
* If the user confirms the operation, the current outbound device is
* deleted.
*
* @param outboundDevice
* the device being removed
* @param activity
* the caller activity
*/
public static void confirmRemoveOutbound(final PairedDevice outboundDevice,
final MainActivity activity) {
new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
// @formatter:off
AlertDialog.Builder builder = getBuilder(activity)
.setMessage(
getResources()
.getString(
R.string.remove_outbound_paired_message,
outboundDevice.type))
.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
activity.deleteOutboundDevice();
}
})
.setNegativeButton(android.R.string.cancel, null);
// @formatter:on
return builder.create();
}
}.show(activity.getSupportFragmentManager(), "removeInbound");
}
/**
* Shows a dialog telling the user what to do before the QR code scanner is
* displayed, and starts the QR code activity.
*
* @param activity
* the caller activity
*/
public static void pairInboundInstructions(final FragmentActivity activity) {
DialogFragment dialog = new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = getBuilder(activity);
View layout = View.inflate(getContext(),
R.layout.pair_inbound_instructions, null);
builder.setView(layout);
((TextView) layout.findViewById(R.id.instructions))
.setText(getResources().getString(
R.string.new_inbound_instructions));
builder.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
Intent intent = new Intent(
"com.google.zxing.client.android.SCAN");
intent.putExtra(
"com.google.zxing.client.android.SCAN.SCAN_MODE",
"QR_CODE_MODE");
getActivity().startActivityForResult(intent,
MainActivity.QR_CODE_SCANNED);
}
});
return builder.create();
}
};
dialog.show(activity.getSupportFragmentManager(), "instruction");
}
/**
* Asks the user for confirmation of a delete inbound device operation.
*
* <p>
* If the user confirms the operation, the device is removed from the list
* of paired devices.
*
* @param deviceToBeUnpaired
* the device to be removed from the list of paired devices
* @param activity
* the caller activity
*/
public static void confirmUnpairInbound(
final PairedDevice deviceToBeUnpaired, final MainActivity activity) {
new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
// @formatter:off
AlertDialog.Builder builder = getBuilder(activity)
.setMessage(
getResources().getString(
R.string.remove_inbound_paired_message,
deviceToBeUnpaired.name))
.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
activity.removePaired();
}
})
.setNegativeButton(android.R.string.cancel, null);
// @formatter:on
return builder.create();
}
}.show(activity.getSupportFragmentManager(), "removeInbound");
}
/**
* Asks the user for confirmation of a delete all inbound devices operation.
*
* <p>
* If the user confirms the operation, all devices are removed from the list
* of inbound paired devices.
*
* @param activity
* the caller activity
*/
public static void confirmUnpairAllInbound(final MainActivity activity) {
new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
// @formatter:off
AlertDialog.Builder builder = getBuilder(activity)
.setMessage(
getResources().getString(
R.string.delete_all_inbound_confirm))
.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
activity.deleteAllInbound();
}
})
.setNegativeButton(android.R.string.cancel, null);
// @formatter:on
return builder.create();
}
}.show(activity.getSupportFragmentManager(), "removeAllInbound");
}
/**
* Shows a dialog informing the user that no outbound device is currently
* configured, and takes the user to the {@link PairOutboundActivity}.
*
* @param activity
* the caller activity
*/
public static void noPairedDevice(final FragmentActivity activity) {
new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = getNoUiBuilder(activity);
builder.setMessage(getString(R.string.no_paired_device));
builder.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
Intent i = new Intent(activity,
PairOutboundActivity.class);
startActivity(i);
}
});
Dialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
}.show(activity.getSupportFragmentManager(), "no paired device");
}
/**
* Shows a dialog informing the user that the action she's trying to perform
* requires an Internet connection which is not available at the moment.
*
* @param activity
* the caller activity
* @param whyItsNeeded
* the resource ID within <tt>strings.xml</tt> that explains to
* the user why an Internet connection is needed by the operation
* @param finishActivityOnOk
* whether the caller activity must be {@link Activity#finish()}
* 'ed after the user hides the dialog
*/
public static void noInternetConnection(final FragmentActivity activity,
final int whyItsNeeded, final boolean finishActivityOnOk) {
new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder;
if (finishActivityOnOk) {
builder = getNoUiBuilder(activity);
} else {
builder = getBuilder(activity);
}
builder.setMessage(getString(R.string.no_internet_connection,
getString(whyItsNeeded)));
Dialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(!finishActivityOnOk);
return dialog;
}
}.show(activity.getSupportFragmentManager(), "no_internet");
}
/**
* Shows a dialog informing the user that Whatsapp is not installed on the
* device, and gives an option to hide the dialog in the future.
*
* @param activity
* the caller activity
* @param intent
* the intent containing the content to be shared
*/
public static void whatsappMissing(final SendToWhatsappActivity activity,
final Intent intent) {
// @formatter:off
DialogFragment dialog = new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = getNoUiBuilder(activity)
.setTitle(
activity.getString(R.string.whatsapp_not_installed_title))
.setMessage(
activity.getString(R.string.whatsapp_not_installed))
.setPositiveButton(
activity.getString(android.R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
activity.startActivity(SendToAppActivity.createPlainIntent(intent
.getStringExtra("message")));
}
})
.setNegativeButton(
activity.getString(R.string.whatsapp_not_installed_dont_mind),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
SharedPreferences pref = activity
.getSharedPreferences(
"it.mb.whatshare",
Context.MODE_PRIVATE);
pref.edit()
.putBoolean(
SendToWhatsappActivity.HIDE_MISSING_WHATSAPP_KEY,
true).commit();
activity.startActivity(SendToAppActivity.createPlainIntent(intent
.getStringExtra("message")));
}
});
Dialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
return dialog;
}
};
dialog.show(activity.getSupportFragmentManager(), "whatsapp_missing");
// @formatter:on
}
/**
* Shows the about screen.
*
* @param activity
* the caller activity
*/
public static void showAbout(final FragmentActivity activity) {
DialogFragment dialog = new PatchedDialogFragment() {
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = getBuilder(activity);
View layout = View.inflate(getContext(), R.layout.about, null);
String version = "alpha";
try {
version = activity.getPackageManager().getPackageInfo(
activity.getPackageName(), 0).versionName;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
((TextView) layout.findViewById(R.id.title)).setText(activity
.getResources().getString(R.string.title_about_dialog,
version));
((TextView) layout.findViewById(R.id.build_date))
.setText(activity.getResources().getString(
R.string.build_date_about,
getBuildDate(activity)));
// make links clickable
((TextView) layout.findViewById(R.id.description))
.setMovementMethod(LinkMovementMethod.getInstance());
builder.setView(layout);
return builder.create();
}
};
dialog.show(activity.getSupportFragmentManager(), "about");
}
private static String getBuildDate(final Activity activity) {
String buildDate = "";
ZipFile zf = null;
try {
ApplicationInfo ai = activity.getPackageManager()
.getApplicationInfo(activity.getPackageName(), 0);
zf = new ZipFile(ai.sourceDir);
ZipEntry ze = zf.getEntry("classes.dex");
long time = ze.getTime();
buildDate = SimpleDateFormat.getInstance().format(
new java.util.Date(time));
} catch (Exception e) {
} finally {
if (zf != null) {
try {
zf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return buildDate;
}
}