net.hockeyapp.android.FeedbackManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of HockeySDK Show documentation
Show all versions of HockeySDK Show documentation
HockeySDK-Android implements support for using HockeyApp in your Android application. The following features are currently supported:
Collect crash reports:If your app crashes, a crash log is written to the device's storage. If the user starts the app again, they will be asked asked to submit the crash report to HockeyApp. This works for both beta and live apps, i.e. those submitted to Google Play or other app stores. Crash logs contain viable information for you to help resolve the issue. Furthermore, you as a developer can add additional information to the report as well.
Update Alpha/Beta apps: The app will check with HockeyApp if a new version for your alpha/beta build is available. If yes, it will show a dialog to users and let them see the release notes, the version history and start the installation process right away. You can even force the installation of certain updates.
User Metrics: Understand user behavior to improve your app. Track usage through daily and monthly active users. Monitor crash impacted users. Measure customer engagement through session count. Add custom tracking calls to learn which features your users are actually using. This feature requires a minimum API level of 14 (Android 4.x Ice Cream Sandwich).
Feedback: Besides crash reports, collecting feedback from your users from within your app is a great option to help with improving your app. You act on and answer feedback directly from the HockeyApp backend.
Authenticate: Identify and authenticate users against your registered testers with the HockeyApp backend.
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.util.Log;
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.PrefsUtil;
import net.hockeyapp.android.utils.Util;
import java.io.File;
import java.io.FileOutputStream;
/**
* Description
*
* The FeedbackManager displays the feedback currentActivity.
*
* License
*
*
* Copyright (c) 2011-2014 Bit Stadium GmbH
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
*
* @author Bogdan Nistor
**/
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;
/**
* Whether the user's email is required.
*/
private static FeedbackUserDataElement requireUserEmail;
/**
* Last listener instance.
*/
private static FeedbackManagerListener lastListener = null;
/**
* 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;
}
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("url", getURLString(context));
intent.putExtra("initialAttachments", 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("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;
}
/**
* 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() {
@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 (Exception e) {
Log.e(Constants.TAG, "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.", 2000).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.", 2000).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) {
Log.i(Constants.TAG, String.format("Scanned path %s -> URI = %s", path, uri.toString()));
connection.disconnect();
}
}
}