it.mb.whatshare.Dialogs.java Source code

Java tutorial

Introduction

Here is the source code for it.mb.whatshare.Dialogs.java

Source

/**
 * 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 &lt;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;
    }
}