/*
* Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.achep.base.ui.fragments.dialogs;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.AdapterView;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Spinner;
import com.achep.acdisplay.Config;
import com.achep.acdisplay.R;
import com.achep.acdisplay.providers.LogAttachmentProvider;
import com.achep.acdisplay.ui.DialogHelper;
import com.achep.base.Build;
import com.achep.base.Device;
import com.achep.base.content.ConfigBase;
import com.achep.base.providers.LogsProviderBase;
import com.achep.base.utils.FileUtils;
import com.achep.base.utils.IntentUtils;
import com.achep.base.utils.PackageUtils;
import com.achep.base.utils.ResUtils;
import com.achep.base.utils.ToastUtils;
import com.achep.base.utils.ViewUtils;
import com.achep.base.utils.logcat.Logcat;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import static com.achep.base.Build.DEBUG;
/**
* Feedback dialog fragment.
* <p/>
* Provides an UI for sending bugs & suggestions on my email.
*/
public class FeedbackDialog extends DialogFragment implements ConfigBase.OnConfigChangedListener {
private View mFaqContainer;
private Spinner mSpinner;
private EditText mEditText;
private CheckBox mAttachLogCheckBox;
private final AdapterView.OnItemSelectedListener mListener =
new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// Show "Attach log" checkbox only if the type
// of this message is "Issue".
ViewUtils.setVisible(mAttachLogCheckBox, position == 0);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
ViewUtils.setVisible(mAttachLogCheckBox, false);
}
};
@Override
public void onResume() {
super.onResume();
Config config = Config.getInstance();
config.registerListener(this);
updateFaqPanel(config.getTriggers().isHelpRead());
}
@Override
public void onPause() {
Config config = Config.getInstance();
config.unregisterListener(this);
super.onPause();
}
@Override
public void onConfigChanged(@NonNull ConfigBase config,
@NonNull String key,
@NonNull Object value) {
switch (key) {
case Config.KEY_TRIG_HELP_READ:
boolean read = (boolean) value;
updateFaqPanel(read);
break;
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Activity activity = getActivity();
assert activity != null;
MaterialDialog md = new MaterialDialog.Builder(activity)
.iconRes(R.drawable.ic_email_white_24dp)
.title(R.string.feedback_dialog_title)
.customView(R.layout.feedback_dialog, true)
.negativeText(android.R.string.cancel)
.positiveText(R.string.send)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog materialDialog,
@NonNull DialogAction dialogAction) {
Context context = getActivity();
CharSequence message = mEditText.getText();
if (isMessageLongEnough(message)) {
boolean attachLog = mAttachLogCheckBox.isChecked()
&& mAttachLogCheckBox.getVisibility() == View.VISIBLE;
int type = mSpinner.getSelectedItemPosition();
CharSequence title = createTitle(context, type);
CharSequence body = createBody(context, message);
send(title, body, attachLog);
} else {
String toastText = getString(
R.string.feedback_error_msg_too_short,
getMinMessageLength());
ToastUtils.showShort(context, toastText);
}
}
})
.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog materialDialog,
@NonNull DialogAction dialogAction) {
dismiss();
}
})
.autoDismiss(false)
.build();
View view = md.getCustomView();
assert view != null;
mSpinner = (Spinner) view.findViewById(R.id.type);
mSpinner.setOnItemSelectedListener(mListener);
mEditText = (EditText) view.findViewById(R.id.message);
mAttachLogCheckBox = (CheckBox) view.findViewById(R.id.checkbox);
// Frequently asked questions panel
Config.Triggers triggers = Config.getInstance().getTriggers();
if (!triggers.isHelpRead()) initFaqPanel((ViewGroup) view);
return md;
}
/**
* Initialize Frequently asked questions panel. This panel is here to reduce
* the number of already answered questions.
*/
private void initFaqPanel(@NonNull ViewGroup root) {
mFaqContainer = ((ViewStub) root.findViewById(R.id.faq)).inflate();
mFaqContainer.findViewById(R.id.faq).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AppCompatActivity activity = (AppCompatActivity) getActivity();
DialogHelper.showHelpDialog(activity);
}
});
}
/**
* Removes Frequently asked questions panel from the view
* and sets {@link #mFaqContainer} to null.
* After calling this method you no longer able to get panel back.
*/
private void recycleFaqPanel() {
ViewUtils.removeViewParent(mFaqContainer);
mFaqContainer = null;
}
/**
* {@link #recycleFaqPanel() Recycles} Frequently asked questions panel when it's not needed
* anymore.
*
* @param isHelpRead {@code true} to recycle panel, {@code false} to do nothing.
*/
private void updateFaqPanel(boolean isHelpRead) {
if (mFaqContainer != null && isHelpRead) {
recycleFaqPanel();
}
}
private void send(@NonNull CharSequence title,
@NonNull CharSequence body, boolean attachLog) {
Activity context = getActivity();
String[] recipients = {Build.SUPPORT_EMAIL};
Intent intent = new Intent()
.putExtra(Intent.EXTRA_EMAIL, recipients)
.putExtra(Intent.EXTRA_SUBJECT, title)
.putExtra(Intent.EXTRA_TEXT, body);
if (attachLog) {
attachLog(intent);
intent.setAction(Intent.ACTION_SEND);
intent.setType("message/rfc822");
} else {
intent.setAction(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:")); // only email apps should handle it
}
if (IntentUtils.hasActivityForThat(context, intent)) {
startActivity(intent);
dismiss();
} else {
ToastUtils.showLong(context, R.string.feedback_error_no_app);
}
}
/**
* Creates the title of the email.
*
* @param type one of the following types:
* 0 - issue
* 1 - suggestion
* 2 - other
* @return the title of the email.
*/
@NonNull
private CharSequence createTitle(@NonNull Context context, int type) {
CharSequence osVersion = Device.API_VERSION_NAME_SHORT;
CharSequence[] types = new CharSequence[]{"issue", "suggestion", "other"};
return AboutDialog.getVersionName(context) + ": " + osVersion + ", " + types[type];
}
/**
* Creates the body of the email. It automatically adds some
* info about the device.
*
* @param msg the message that been typed by user.
* @return the body of the email
*/
@NonNull
private CharSequence createBody(@NonNull Context context, @NonNull CharSequence msg) {
final String extra;
do {
PackageInfo pi;
try {
pi = context
.getPackageManager()
.getPackageInfo(PackageUtils.getName(context), 0);
} catch (PackageManager.NameNotFoundException e) {
extra = "There was an exception while getting my own package info.";
break;
}
JSONObject obj = new JSONObject();
try {
Config config = Config.getInstance();
// App related stuff
obj.put("app_version_code", pi.versionCode);
obj.put("app_version_name", pi.versionName);
obj.put("app_timestamp", Build.TIME_STAMP);
obj.put("app_is_debug", DEBUG);
obj.put("app_is_help_read", config.getTriggers().isHelpRead());
obj.put("app_launch_count", config.getTriggers().getLaunchCount());
// Device related stuff
obj.put("language", Locale.getDefault().getLanguage());
obj.put("android_version_release", android.os.Build.VERSION.RELEASE);
obj.put("android_version_sdk_int", android.os.Build.VERSION.SDK_INT);
obj.put("android_build_display", android.os.Build.DISPLAY);
obj.put("android_build_brand", android.os.Build.BRAND);
obj.put("android_build_model", android.os.Build.MODEL);
} catch (JSONException ignored) {
extra = "There was an exception while building JSON.";
break;
}
extra = obj.toString().replaceAll(",\"", ", \"");
} while (false);
return msg + "\n\nExtras (added automatically & do not change):\n" + extra;
}
private boolean isMessageLongEnough(@Nullable CharSequence message) {
return message != null && message.length() >= getMinMessageLength();
}
private int getMinMessageLength() {
return getResources().getInteger(R.integer.config_feedback_minMessageLength);
}
private void attachLog(@NonNull Intent intent) {
Context context = getActivity();
try {
String log = Logcat.capture();
if (log == null)
throw new Exception("Failed to capture the logcat.");
// Prepare cache directory.
File cacheDir = context.getCacheDir();
if (cacheDir == null)
throw new Exception("Cache directory is inaccessible");
File directory = new File(cacheDir, LogsProviderBase.DIRECTORY);
FileUtils.deleteRecursive(directory); // Clean-up cache folder
if (!directory.mkdirs())
throw new Exception("Failed to create cache directory.");
// Create log file.
@SuppressLint("SimpleDateFormat")
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String fileName = "AcDisplay_log_" + sdf.format(new Date()) + ".txt";
File file = new File(directory, fileName);
// Write to the file.
if (!FileUtils.writeToFile(file, log))
throw new Exception("Failed to write log to the file.");
// Put extra stream to the intent.
Uri uri = Uri.parse("content://" + LogAttachmentProvider.AUTHORITY + "/" + fileName);
intent.putExtra(Intent.EXTRA_STREAM, uri);
} catch (Exception e) {
String message = ResUtils.getString(getResources(), R.string.feedback_error_accessing_log, e.getMessage());
ToastUtils.showLong(context, message);
}
}
}