package net.hockeyapp.android; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.graphics.Bitmap; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.view.View; import android.widget.Toast; import net.hockeyapp.android.objects.FeedbackUserDataElement; import net.hockeyapp.android.tasks.ParseFeedbackTask; import net.hockeyapp.android.tasks.SendFeedbackTask; import net.hockeyapp.android.utils.AsyncTaskUtils; import net.hockeyapp.android.utils.HockeyLog; import net.hockeyapp.android.utils.PrefsUtil; import net.hockeyapp.android.utils.Util; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; /** * <h3>Description</h3> * * The FeedbackManager displays the feedback currentActivity. * **/ public class FeedbackManager { /** * The id of the notification to take a screenshot. */ private static final int SCREENSHOT_NOTIFICATION_ID = 1; /** * The request code for the broadcast. */ private static final int BROADCAST_REQUEST_CODE = 1; /** * Broadcast action for intent filter. */ private static final String BROADCAST_ACTION = "net.hockeyapp.android.SCREENSHOT"; /** * The BroadcastReceiver instance to listen to the screenshot notification broadcast. */ private static BroadcastReceiver receiver = null; /** * Used to hold a reference to the currently visible currentActivity of this app. */ private static Activity currentActivity; /** * Tells if a notification has been created and is visible to the user. */ private static boolean notificationActive = false; /** * App identifier from HockeyApp. */ private static String identifier = null; /** * URL of HockeyApp service. */ private static String urlString = null; /** * Whether the user's name is required. */ private static FeedbackUserDataElement requireUserName = FeedbackUserDataElement.REQUIRED; /** * Whether the user's email is required. */ private static FeedbackUserDataElement requireUserEmail = FeedbackUserDataElement.REQUIRED; private static String userName; private static String userEmail; /** * Last listener instance. */ private static FeedbackManagerListener lastListener = null; /** * Registers new Feedback manager. * HockeyApp App Identifier is read from configuration values in AndroidManifest.xml * * @param context The context to use. Usually your Activity object. */ public static void register(Context context) { String appIdentifier = Util.getAppIdentifier(context); register(context, appIdentifier); } /** * Registers new Feedback manager. * HockeyApp App Identifier is read from configuration values in AndroidManifest.xml * * @param context The context to use. Usually your Activity object. * @param listener Implement for callback functions. */ public static void register(Context context, FeedbackManagerListener listener) { String appIdentifier = Util.getAppIdentifier(context); register(context, appIdentifier, listener); } /** * Registers new Feedback manager. * * @param context The context to use. Usually your Activity object. * @param appIdentifier App ID of your app on HockeyApp. */ public static void register(Context context, String appIdentifier) { register(context, appIdentifier, null); } /** * Registers new Feedback manager. * * @param context The context to use. Usually your Activity object. * @param appIdentifier App ID of your app on HockeyApp. * @param listener Implement for callback functions. */ public static void register(Context context, String appIdentifier, FeedbackManagerListener listener) { register(context, Constants.BASE_URL, appIdentifier, listener); } /** * Registers new Feedback manager. * * @param context The context to use. Usually your Activity object. * @param urlString URL of the HockeyApp server. * @param appIdentifier App ID of your app on HockeyApp. * @param listener Implement for callback functions. */ public static void register(Context context, String urlString, String appIdentifier, FeedbackManagerListener listener) { if (context != null) { FeedbackManager.identifier = Util.sanitizeAppIdentifier(appIdentifier); FeedbackManager.urlString = urlString; FeedbackManager.lastListener = listener; Constants.loadFromContext(context); } } /** * Unregisters the update manager */ public static void unregister() { lastListener = null; } /** * Starts the {@link FeedbackActivity} * * @param context {@link Context} object * @param attachments the optional attachment {@link Uri}s */ public static void showFeedbackActivity(Context context, Uri... attachments) { showFeedbackActivity(context, null, attachments); } /** * Starts the {@link FeedbackActivity} * * @param context {@link Context} object * @param attachments the optional attachment {@link Uri}s * @param extras a bundle to be added to the Intent that starts the FeedbackActivity instance */ public static void showFeedbackActivity(Context context, Bundle extras, Uri... attachments) { if (context != null) { Class<?> activityClass = null; if (lastListener != null) { activityClass = lastListener.getFeedbackActivityClass(); } if (activityClass == null) { activityClass = FeedbackActivity.class; } boolean forceNewThread = lastListener != null && lastListener.shouldCreateNewFeedbackThread(); Intent intent = new Intent(); if (extras != null && !extras.isEmpty()) { intent.putExtras(extras); } intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setClass(context, activityClass); intent.putExtra(FeedbackActivity.EXTRA_URL, getURLString(context)); intent.putExtra(FeedbackActivity.EXTRA_FORCE_NEW_THREAD, forceNewThread); intent.putExtra(FeedbackActivity.EXTRA_INITIAL_USER_NAME, userName); intent.putExtra(FeedbackActivity.EXTRA_INITIAL_USER_EMAIL, userEmail); intent.putExtra(FeedbackActivity.EXTRA_INITIAL_ATTACHMENTS, attachments); context.startActivity(intent); } } /** * Checks if an answer to the feedback is available and if yes notifies the listener or * creates a system notification. * * @param context the context to use */ public static void checkForAnswersAndNotify(final Context context) { String token = PrefsUtil.getInstance().getFeedbackTokenFromPrefs(context); if (token == null) { return; } int lastMessageId = context.getSharedPreferences(ParseFeedbackTask.PREFERENCES_NAME, 0) .getInt(ParseFeedbackTask.ID_LAST_MESSAGE_SEND, -1); SendFeedbackTask sendFeedbackTask = new SendFeedbackTask(context, getURLString(context), null, null, null, null, null, token, new Handler() { @Override public void handleMessage(Message msg) { Bundle bundle = msg.getData(); String responseString = bundle.getString(SendFeedbackTask.BUNDLE_FEEDBACK_RESPONSE); if (responseString != null) { ParseFeedbackTask task = new ParseFeedbackTask(context, responseString, null, "fetch"); task.setUrlString(getURLString(context)); AsyncTaskUtils.execute(task); } } }, true); sendFeedbackTask.setShowProgressDialog(false); sendFeedbackTask.setLastMessageId(lastMessageId); AsyncTaskUtils.execute(sendFeedbackTask); } /** * Returns the last listener which has been registered with any Feedback manager. * * @return last used feedback listener */ public static FeedbackManagerListener getLastListener() { return lastListener; } /** * Populates the URL String with the appIdentifier * * @param context {@link Context} object * @return */ private static String getURLString(Context context) { return urlString + "api/2/apps/" + identifier + "/feedback/"; } /** * Returns the required setting for the user name property. * * @return required setting for name */ public static FeedbackUserDataElement getRequireUserName() { return requireUserName; } /** * Sets the required setting for the user name property * * @param requireUserName whether the user name property should be required */ public static void setRequireUserName(FeedbackUserDataElement requireUserName) { FeedbackManager.requireUserName = requireUserName; } /** * Returns the required setting for the user email property. * * @return required setting for email */ public static FeedbackUserDataElement getRequireUserEmail() { return requireUserEmail; } /** * Sets the required setting for the user email property * * @param requireUserEmail whether the user email property should be required */ public static void setRequireUserEmail(FeedbackUserDataElement requireUserEmail) { FeedbackManager.requireUserEmail = requireUserEmail; } /** * Sets the user's name to pre-fill the feedback form with * * @param userName user's name */ public static void setUserName(String userName) { FeedbackManager.userName = userName; } /** * Sets the user's e-mail to pre-fill the feedback form with * * @param userEmail user's e-mail */ public static void setUserEmail(String userEmail) { FeedbackManager.userEmail = userEmail; } /** * Stores a reference to the given activity to be used for taking a screenshot of it. * Reference is cleared only when method unsetCurrentActivityForScreenshot is called. * * @param activity {@link Activity} object */ public static void setActivityForScreenshot(Activity activity) { currentActivity = activity; if (!notificationActive) { startNotification(); } } /** * Clears the reference to the activity that was set before by setActivityForScreenshot. * * @param activity activity for screenshot */ public static void unsetCurrentActivityForScreenshot(Activity activity) { if (currentActivity == null || currentActivity != activity) { return; } endNotification(); currentActivity = null; } /** * Takes a screenshot of the currently set activity and stores it in the HockeyApp folder on the * external storage also publishing it to the Android gallery. * * @param context toast messages will be displayed using this context */ public static void takeScreenshot(final Context context) { View view = currentActivity.getWindow().getDecorView(); view.setDrawingCacheEnabled(true); final Bitmap bitmap = view.getDrawingCache(); String filename = currentActivity.getLocalClassName(); File dir = Constants.getHockeyAppStorageDir(); File result = new File(dir, filename + ".jpg"); int suffix = 1; while (result.exists()) { result = new File(dir, filename + "_" + suffix + ".jpg"); suffix++; } new AsyncTask<File, Void, Boolean>() { @Override protected Boolean doInBackground(File... args) { try { FileOutputStream out = new FileOutputStream(args[0]); bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); out.close(); return true; } catch (IOException e) { HockeyLog.error("Could not save screenshot.", e); } return false; } @Override protected void onPostExecute(Boolean success) { if (success == false) { Toast.makeText(context, "Screenshot could not be created. Sorry.", Toast.LENGTH_LONG) .show(); } } }.execute(result); /* Publish to gallery. */ MediaScannerClient client = new MediaScannerClient(result.getAbsolutePath()); MediaScannerConnection connection = new MediaScannerConnection(currentActivity, client); client.setConnection(connection); connection.connect(); Toast.makeText(context, "Screenshot '" + result.getName() + "' is available in gallery.", Toast.LENGTH_LONG).show(); } @SuppressWarnings("deprecation") private static void startNotification() { notificationActive = true; NotificationManager notificationManager = (NotificationManager) currentActivity.getSystemService(Context.NOTIFICATION_SERVICE); int iconId = currentActivity.getResources().getIdentifier("ic_menu_camera", "drawable", "android"); Intent intent = new Intent(); intent.setAction(BROADCAST_ACTION); PendingIntent pendingIntent = PendingIntent.getBroadcast(currentActivity, BROADCAST_REQUEST_CODE, intent, PendingIntent.FLAG_ONE_SHOT); Notification notification = Util.createNotification(currentActivity, pendingIntent, "HockeyApp Feedback", "Take a screenshot for your feedback.", iconId); notificationManager.notify(SCREENSHOT_NOTIFICATION_ID, notification); if (receiver == null) { receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { FeedbackManager.takeScreenshot(context); } }; } currentActivity.registerReceiver(receiver, new IntentFilter(BROADCAST_ACTION)); } private static void endNotification() { notificationActive = false; currentActivity.unregisterReceiver(receiver); NotificationManager notificationManager = (NotificationManager) currentActivity.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(SCREENSHOT_NOTIFICATION_ID); } /** * Provides a callback for when the media scanner is connected and an image can be scanned. */ private static class MediaScannerClient implements MediaScannerConnection.MediaScannerConnectionClient { private MediaScannerConnection connection; private String path; private MediaScannerClient(String path) { this.connection = null; this.path = path; } public void setConnection(MediaScannerConnection connection) { this.connection = connection; } @Override public void onMediaScannerConnected() { if (connection != null) { connection.scanFile(path, null); } } @Override public void onScanCompleted(String path, Uri uri) { HockeyLog.verbose(String.format("Scanned path %s -> URI = %s", path, uri.toString())); connection.disconnect(); } } }