Skip to content

Commit

Permalink
Add huge performance improvements
Browse files Browse the repository at this point in the history
- Address potential activity memory leaks, by allowing callers to unregister handlers, listeners or validators. Actually, MaoniActivity will auto-clear strong references when it exits. This requires callers to reset their handlers when they are done. Furthermore, Maoni#start(...) can no longer be called twice.

- Reduce memory footprint when loading image views (especially, screen captures). Thumbnail preview has been reduced to a 100x100 size, since it does not need to be loaded at full resolution.
  • Loading branch information
rm3l committed Aug 10, 2016
1 parent 468b0fd commit 9ad33aa
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class MaoniSampleMainActivity extends AppCompatActivity {
private static final String MY_FILE_PROVIDER_AUTHORITY =
(BuildConfig.APPLICATION_ID + ".fileprovider");

private Maoni mMaoni;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand All @@ -53,30 +55,42 @@ protected void onCreate(Bundle savedInstanceState) {
setSupportActionBar(toolbar);
}

final MyHandlerForMaoni handlerForMaoni = new MyHandlerForMaoni(this);
final Maoni.Builder maoniBuilder = new Maoni.Builder(MY_FILE_PROVIDER_AUTHORITY)
.withWindowTitle("Feedback") //Set to an empty string to clear it
.withMessage("Hey! Love or hate this app? We would love to hear from you.\n\n" +
"Note: Almost everything in Maoni is customizable.")
.withExtraLayout(R.layout.my_feedback_activity_extra_content)
.withFeedbackContentHint("[Custom hint] Write your feedback here")
.withIncludeLogsText("[Custom text] Include system logs")
.withIncludeScreenshotText("[Custom text] Include screenshot")
.withTouchToPreviewScreenshotText("Touch To Preview")
.withContentErrorMessage("Custom error message")
.withScreenshotHint("Custom test: Lorem Ipsum Dolor Sit Amet...");

final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
if (fab != null) {
final MyHandlerForMaoni handlerForMaoni = new MyHandlerForMaoni(this);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new Maoni.Builder(MY_FILE_PROVIDER_AUTHORITY)
.withWindowTitle("Feedback") //Set to an empty string to clear it
.withMessage("Hey! Love or hate this app? We would love to hear from you.\n\n" +
"Note: Almost everything in Maoni is customizable.")
.withExtraLayout(R.layout.my_feedback_activity_extra_content)
.withHandler(handlerForMaoni)
.withFeedbackContentHint("[Custom hint] Write your feedback here")
.withIncludeScreenshotText("[Custom text] Include screenshot")
.withTouchToPreviewScreenshotText("Touch To Preview")
.withContentErrorMessage("Custom error message")
.withScreenshotHint("Custom test: Lorem Ipsum Dolor Sit Amet...")
.build()
.start(MaoniSampleMainActivity.this);
// MaoniActivity de-registers handlers, listeners and validators upon activity destroy,
// so we need to re-register it again by reconstructing a new Maoni instance.
//Also, Maoni.start(...) cannot be called twice,
// but we are reusing the Builder to construct a new instance along with its handler.
mMaoni = maoniBuilder.withHandler(handlerForMaoni).build();
mMaoni.start(MaoniSampleMainActivity.this);
}
});
}
}

@Override
protected void onDestroy() {
//Clear strong references used in Maoni, by de-registering any handlers, listeners and validators
mMaoni.clear();
super.onDestroy();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_maoni_sample, menu);
Expand Down
43 changes: 43 additions & 0 deletions maoni/src/main/java/org/rm3l/maoni/Maoni.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.rm3l.maoni.utils.ViewUtils;

import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.rm3l.maoni.Maoni.CallbacksConfiguration.getInstance;
import static org.rm3l.maoni.ui.MaoniActivity.APPLICATION_INFO_BUILD_CONFIG_BUILD_TYPE;
Expand Down Expand Up @@ -160,6 +161,8 @@ public class Maoni {
private final String fileProviderAuthority;
private File maoniWorkingDir;

private final AtomicBoolean mUsed = new AtomicBoolean(false);

/**
* Constructor
* @param fileProviderAuthority the file provider authority.
Expand Down Expand Up @@ -226,6 +229,14 @@ public Maoni(
* @param callerActivity the caller activity
*/
public void start(@Nullable final Activity callerActivity) {

if (mUsed.getAndSet(true)) {
this.clear();
throw new UnsupportedOperationException(
"Maoni instance cannot be reused to start a new activity. " +
"Please build a new Maoni instance.");
}

if (callerActivity == null) {
Log.d(LOG_TAG, "Target activity is undefined");
return;
Expand Down Expand Up @@ -343,6 +354,32 @@ public void start(@Nullable final Activity callerActivity) {
callerActivity.startActivity(maoniIntent);
}


public Maoni unregisterListener() {
getInstance().setListener(null);
return this;
}

public Maoni unregisterUiListener() {
getInstance().setUiListener(null);
return this;
}

public Maoni unregisterValidator() {
getInstance().setValidator(null);
return this;
}

public Maoni unregisterHandler() {
return this.unregisterListener()
.unregisterUiListener()
.unregisterValidator();
}

public Maoni clear() {
return this.unregisterHandler();
}

/**
* Maoni Builder
*/
Expand Down Expand Up @@ -650,6 +687,12 @@ public CallbacksConfiguration setUiListener(@Nullable final UiListener uiListene
this.uiListener = uiListener;
return this;
}

public CallbacksConfiguration reset() {
return this.setUiListener(null)
.setListener(null)
.setValidator(null);
}
}

}
15 changes: 12 additions & 3 deletions maoni/src/main/java/org/rm3l/maoni/ui/MaoniActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import org.rm3l.maoni.common.contract.Validator;
import org.rm3l.maoni.common.model.Feedback;
import org.rm3l.maoni.utils.LogcatUtils;
import org.rm3l.maoni.utils.ViewUtils;

import java.io.File;
import java.util.UUID;
Expand Down Expand Up @@ -264,9 +265,11 @@ protected void onCreate(Bundle savedInstanceState) {
if (screenshotContentView != null) {
screenshotContentView.setVisibility(View.VISIBLE);
}
final Bitmap mBitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
if (screenshotThumb != null) {
screenshotThumb.setImageBitmap(mBitmap);
//Thumbnail - load with smaller resolution so as to reduce memory footprint
screenshotThumb.setImageBitmap(
ViewUtils.decodeSampledBitmapFromFilePath(
file.getAbsolutePath(), 100, 100));
}

// Hook up clicks on the thumbnail views.
Expand Down Expand Up @@ -299,7 +302,7 @@ public void onClick(View v) {

final ImageView imageView = (ImageView)
imagePreviewDialog.findViewById(R.id.maoni_screenshot_preview_image);
imageView.setImageBitmap(mBitmap);
imageView.setImageURI(Uri.fromFile(file));
imageView.setOnClickListener(clickListener);
imagePreviewDialog.findViewById(R.id.maoni_screenshot_preview_close)
.setOnClickListener(clickListener);
Expand Down Expand Up @@ -369,6 +372,12 @@ public void onClick(View view) {
}
}

@Override
protected void onDestroy() {
CallbacksConfiguration.getInstance().reset();
super.onDestroy();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.maoni_activity_menu, menu);
Expand Down
75 changes: 75 additions & 0 deletions maoni/src/main/java/org/rm3l/maoni/utils/ViewUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
package org.rm3l.maoni.utils;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
Expand Down Expand Up @@ -110,4 +112,77 @@ public static void exportBitmapToFile(@NonNull final Context context,
}
}
}

private static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {

final int halfHeight = height / 2;
final int halfWidth = width / 2;

// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}

return inSampleSize;
}

/**
* Decode a given drawable resource with the specified dimensions
*
* @param res the app resources
* @param resId the drawable resource ID
* @param reqWidth the required width
* @param reqHeight the required height
* @return the bitmap
*/
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {

// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);

// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}

/**
* Decode a given image file with the specified dimensions
*
* @param filePath the file resource path
* @param reqWidth the required width
* @param reqHeight the required height
* @return the bitmap
*/
public static Bitmap decodeSampledBitmapFromFilePath(String filePath,
int reqWidth, int reqHeight) {

// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);

// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filePath, options);

}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
Binary file removed maoni/src/main/res/drawable/maoni_header.png
Binary file not shown.

0 comments on commit 9ad33aa

Please sign in to comment.