/* * Copyright 2014 Sebastiano Poggi and Francesco Pontillo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.frakbot.util.feedback; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.util.Log; import android.view.View; import net.frakbot.util.log.FLog; import java.util.List; /** * Sends a feedback using Android's feedback mechanism (through Google, or any other * vendor's mechanism that implements the Android feedback system interface). * "Inspired" by Google's own way of reporting feedbacks. */ public class FeedbackHelper { private static final String TAG = FeedbackHelper.class.getSimpleName(); private static final Intent BUG_REPORT_INTENT = new Intent("android.intent.action.BUG_REPORT"); public static final int MAX_SCREENSHOT_SIZE = 1048576; /** * Determines if the device supports sending feedback through * Google's own feedback mechanism. * * @param context The context used to test the device. * * @return Returns true if the device supports sending feedback through * Google's own feedback mechanism, false otherwise. */ public static boolean canSendAndroidFeedback(Context context) { // TODO: re-enable Android feedback once it's confirmed it works (and how) on Google's side return false; // canHandleIntent(context, BUG_REPORT_INTENT); } /** * Checks if there is any service available on the device that can handle * the specified intent. * * @param c The context used to test the device. * @param intent The intent to check support for. * * @return Returns true if the specified intent can be resolved to a service * on the device, false otherwise. */ private static boolean canHandleIntent(Context c, Intent intent) { PackageManager pm = c.getPackageManager(); List<ResolveInfo> services; if (pm != null) { services = pm.queryIntentServices(intent, 1); return !services.isEmpty(); } return false; } /** * Determines if the app is installed from the Google Play Store. * * @return Returns true if the app is installed from the Google * Play Store, or false if it has been installed from other * sources (sideload, other app stores, etc) */ private static boolean isInstalledFromPlayStore(Context c) { PackageManager pm = c.getPackageManager(); if (pm == null) { Log.i(TAG, "Unable to retrieve the PackageManager, assuming app is sideloaded"); return false; } String installationSource = pm.getInstallerPackageName(c.getPackageName()); return "com.android.vending".equals(installationSource) || "com.google.android.feedback".equals(installationSource); // This is for Titanium Backup compatibility } /** * Sends a feedback through Google's own feedback mechanism (or any other * vendor's mechanism that implements the Android feedback system interface) * for the specified Activity. * * @param a The Activity to send a feedback for. */ public static void sendFeedback(Activity a) { if (!canSendAndroidFeedback(a) || !isInstalledFromPlayStore(a)) { Log.w(TAG, "Android feedback mechanism is not available, or app is sideloaded.\n" + "Using email fallback mechanism."); sendFeedbackEmail(a); return; } // Bind to the feedback service (this allows us to send the screenshot as well -- // if we only sent the feedback intent, we couldn't have done it) FeedbackServiceBinder binder = new FeedbackServiceBinder(a); a.bindService(BUG_REPORT_INTENT, binder, Context.BIND_AUTO_CREATE); } /** * Sends a feedback email (fallback for when the Android feedback mechanism * doesn't work or isn't available). */ private static void sendFeedbackEmail(Activity a) { FLog.d(TAG, "Starting feedback email thread"); EmailSender emailSender = new EmailSender(a); Thread t = new Thread(emailSender); t.setPriority(Thread.MIN_PRIORITY); t.setName("FeedbackEmailSender"); t.start(); } /** * A ServiceConnection implementation that fetches and sends the Activity * screenshot to the feedback service (most likely, Google's own). */ static class FeedbackServiceBinder implements ServiceConnection { private final Activity mActivity; /** * Initializes the ServiceConnection instance. * * @param activity The Activity to send a feedback for. */ public FeedbackServiceBinder(Activity activity) { mActivity = activity; } @Override public void onServiceConnected(ComponentName name, IBinder service) { try { Parcel parcel = Parcel.obtain(); Bitmap screenshot = getCurrentScreenshot(mActivity); if (screenshot != null) { screenshot.writeToParcel(parcel, 0); } // Send the screenshot (if any) to the service service.transact(1, parcel, null, 0); } catch (RemoteException e) { Log.e(TAG, "Error connecting to bug report service", e); mActivity.unbindService(this); } catch (Throwable t) { mActivity.unbindService(this); } } @Override public void onServiceDisconnected(ComponentName name) { // Nothing to do here } } /** * Grabs a screenshot of the specified Activity. * * @param activity The Activity to grab a screenshot of. * * @return Returns the captured (and resized, if needed) screenshot * of the Activity, or null if it wasn't captured. */ private static Bitmap getCurrentScreenshot(Activity activity) { try { View currentView = activity.getWindow().getDecorView().getRootView(); if (currentView == null) { return null; } boolean drawingCacheWasEnabled = currentView.isDrawingCacheEnabled(); currentView.setDrawingCacheEnabled(true); Bitmap bitmap = currentView.getDrawingCache(); if (bitmap != null) { bitmap = resizeBitmap(bitmap); } if (!drawingCacheWasEnabled) { // Restore the initial drawing cache state currentView.setDrawingCacheEnabled(false); currentView.destroyDrawingCache(); } return bitmap; } catch (Exception e) { return null; } } /** * Resizes the screenshot if needed by dividing by two its dimensions * until the image raw size is less than or equal to {@link #MAX_SCREENSHOT_SIZE}. * * @param bitmap The bitmap to resize. * * @return Returns the bitmap, resized if needed. The returned bitmap is * always a copy of the original one! */ private static Bitmap resizeBitmap(Bitmap bitmap) { bitmap = bitmap.copy(Bitmap.Config.RGB_565, false); // 2 bytes per pixel int width = bitmap.getWidth(); int height = bitmap.getHeight(); // Divide the bitmap dimensions by two until the resulting image size is less // than MAX_SCREENSHOT_SIZE, at 2 BPP (it's a RGB_565 copy of the original!) while (width * height * 2 > MAX_SCREENSHOT_SIZE) { width /= 2; height /= 2; } // Scale only if needed! if (bitmap.getWidth() != width) { bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); } return bitmap; } }