Unable to load multiple images in a RemoteViewsFactory - java

I'm creating a simple Android widget that fetches and displays some movie covers from a website. It's simple GridView that displays the images. The widget works fine but the moment I begin scrolling, I get and OutOfMemoryError and Android kills my process.
Here's the code of my RemoteViewsFactory. I can't seem to understand how to resolve this. The images I'm using are properly sized so I don't waste memory in Android. They are perfectly sized. As you can see I'm using a very small LRU cache and in my manifest, I've even enabled android:largeHeap="true". I've racked my head on this issue for a full two days and I'm wondering if what I'm trying to do is even possible?
public class SlideFactory implements RemoteViewsFactory {
private JSONArray jsoMovies = new JSONArray();
private Context ctxContext;
private Integer intInstance;
private RemoteViews remView;
private LruCache<String, Bitmap> lruCache;
private static String strCouch = "http://192.168.1.110:5984/movies/";
public SlideFactory(Context ctxContext, Intent ittIntent) {
this.ctxContext = ctxContext;
this.intInstance = ittIntent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
this.lruCache = new LruCache<String, Bitmap>(4 * 1024 * 1024);
final Integer intMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final Integer intBuffer = intMemory / 8;
lruCache = new LruCache<String, Bitmap>(intBuffer) {
#Override
protected int sizeOf(String strDigest, Bitmap bmpCover) {
return bmpCover.getByteCount() / 1024;
}
};
}
public RemoteViews getViewAt(int intPosition) {
if (intPosition <= getCount()) {
try {
String strDocid = this.jsoMovies.getJSONObject(intPosition).getString("id");
String strDigest = this.jsoMovies.getJSONObject(intPosition).getJSONObject("value").getJSONObject("_attachments").getJSONObject("thumb.jpg").getString("digest");
String strTitle = this.jsoMovies.getJSONObject(intPosition).getJSONObject("value").getString("title");
Bitmap bmpThumb = this.lruCache.get(strDigest);
if (bmpThumb == null) {
String strUrl = strCouch + strDocid + "/thumb.jpg";
System.out.println("Fetching" + intPosition);
bmpThumb = new ImageFetcher().execute(strUrl).get();
this.lruCache.put(strDigest, bmpThumb);
}
remView.setImageViewBitmap(R.id.movie_cover, bmpThumb);
remView.setTextViewText(R.id.movie_title, strTitle);
} catch (Exception e) {
e.printStackTrace();
}
return remView;
}
return null;
}
public void onCreate() {
return;
}
public void onDestroy() {
jsoMovies = null;
}
public int getCount() {
return 20;
}
public RemoteViews getLoadingView() {
return null;//new RemoteViews(this.ctxContext.getPackageName(), R.layout.loading);
}
public int getViewTypeCount() {
return 1;
}
public long getItemId(int intPosition) {
return intPosition;
}
public boolean hasStableIds() {
return true;
}
public void onDataSetChanged() {
this.remView = new RemoteViews(this.ctxContext.getPackageName(), R.layout.slide);
try {
DefaultHttpClient dhcNetwork = new DefaultHttpClient();
String strUrl = strCouch + "_design/application/_view/language?" + URLEncoder.encode("descending=true&startkey=[\"hi\", {}]&attachments=true");
HttpGet getMovies = new HttpGet(strUrl);
HttpResponse resMovies = dhcNetwork.execute(getMovies);
Integer intMovies = resMovies.getStatusLine().getStatusCode();
if (intMovies != HttpStatus.SC_OK) {
throw new HttpResponseException(intMovies, "Server responded with an error");
}
String strMovies = EntityUtils.toString(resMovies.getEntity(), "UTF-8");
this.jsoMovies = new JSONObject(strMovies).getJSONArray("rows");
} catch (Exception e) {
Log.e("SlideFactory", "Unknown error encountered", e);
}
}
}
Here's the source of the AsyncTask that fetches the images:
public class ImageFetcher extends AsyncTask<String, Void, Bitmap> {
#Override
protected Bitmap doInBackground(String... strUrl) {
Bitmap bmpThumb = null;
try {
URL urlThumb = new URL(strUrl[0]);
HttpURLConnection hucConnection = (HttpURLConnection) urlThumb.openConnection();
InputStream istThumb = hucConnection.getInputStream();
bmpThumb = BitmapFactory.decodeStream(istThumb);
istThumb.close();
hucConnection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
return bmpThumb;
}
}

I had similar bitter experience and after lots of digging I found that setImageViewBitmap copies all bitmap into new instance, so taking double memory.
Consider changing following line into either static resource or something else !
remView.setImageViewBitmap(R.id.movie_cover, bmpThumb);
This takes lots of memory and pings garbage collector to clean memory, but so slow that your app can't use the freed memory in time.

My workaround is using LRUCache to store widget's bitmaps:
First, init as usual:
protected void initializeCache()
{
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/10th of the available memory for this memory cache.
final int cacheSize = maxMemory / 10;
bitmapLruCache = new LruCache<String, Bitmap>(cacheSize)
{
#Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
}
Then reuse bitmaps:
void updateImageView(RemoteViews views, int resourceId, final String imageUrl)
{
Bitmap bitmap = bitmapLruCache.get(imageUrl);
if (bitmap == null)
{
bitmap = // get bitmap from web with your loader
bitmapLruCache.put(imageUrl, bitmap);
}
views.setImageViewBitmap(resourceId, bitmap);
}
With this code widget does not crash my app now.
More info here

Related

Progress indicator on RecyclerView

I work with a RecyclerView that looks like this.
I use an AsyncTask for managing the downloads. I use this button so that each item in the list of cards can have the progress of the respective download. I am not sure how to report the status of the download to the RecyclerView. How do I get this to post updates to the cards?
The async downloader code is this
public class DownloadFileFromURL extends AsyncTask<String, String, String> {
private final String resourceType;
public DownloadFileFromURL(String resourceType) {
super();
this.resourceType = resourceType;
// do stuff
}
#Override
protected void onPreExecute() {
super.onPreExecute();
//showDialog(progress_bar_type);
}
protected void onProgressUpdate(String... progress) {
// setting progress percentage
// pDialog.setProgress(Integer.parseInt(progress[0]));
}
#Override
protected String doInBackground(String... f_url) {
int count;
try {
URL url = new URL(f_url[0]);
String fileName = url.toString().substring(url.toString().lastIndexOf('/') + 1);
URLConnection connection = url.openConnection();
connection.connect();
// this will be useful so that you can show a tipical 0-100%
// progress bar
int lengthOfFile = connection.getContentLength();
Log.d("lengthofFile", String.valueOf(lengthOfFile));
// download the file
InputStream input = new BufferedInputStream(url.openStream(),
8192);
String destinationDirectory ="";
if(resourceType.equals(SyncUtil.IMAGE_ZIP)) {
destinationDirectory= SyncUtil.TMP;
}
if(resourceType.equals(SyncUtil.VIDEOFILE)) {
destinationDirectory = SyncUtil.VIDEO;
}
File mFolder = new File(AppController.root.toString() + File.separator+destinationDirectory);
if (!mFolder.exists()) {
mFolder.mkdir();
}
OutputStream output = new FileOutputStream(AppController.root.toString()+File.separator+destinationDirectory+File.separator
+ fileName);
byte data[] = new byte[1024];
long total = 0;
while ((count = input.read(data)) != -1) {
total += count;
// publishing the progress....
// After this onProgressUpdate will be called
publishProgress("" + (int) ((total * 100) / lengthOfFile));
output.write(data, 0, count);
}
output.flush();
// closing streams
output.close();
input.close();
if(resourceType.equals(SyncUtil.IMAGE_ZIP)) {
BusProvider.getInstance().post(new ZipDownloadComplete(fileName,resourceType));
}
if(resourceType.equals(SyncUtil.VIDEOFILE)) {
// BusProvider.getInstance().post(new VideoDownloadComplete(fileName));
}
} catch (Exception e) {
Log.e("Error: ", e.getMessage());
}
return null;
}
#Override
protected void onPostExecute(String file_url) {
}
}
The RecyclerView adapter is here
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final Video video = videosList.get(position);
holder.title.setText(video.getTitle());
holder.description.setText(video.getDescription());
holder.downloadButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
String url ="http://"+ AppController.serverAddr +":"+AppController.port +"/video/"+video.getUrl()+video.getExtension();
DownloadFileFromURL downloadFileFromURL = new DownloadFileFromURL(SyncUtil.VIDEOFILE);
downloadFileFromURL.execute(url,video.getTitle(),video.getDescription());
}
});
holder.bind(video,listener);
}
Though its not a very good solution but in my case I got that working. I'm just sharing my thoughts with some sample code snippet.
I assume you're showing the download progress with a ProgressBar. So take an instance of the ProgressBar in your adapter and pass the reference to your AsyncTask.
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final Video video = videosList.get(position);
holder.title.setText(video.getTitle());
holder.description.setText(video.getDescription());
holder.downloadButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
String url ="http://"+ AppController.serverAddr +":"+AppController.port +"/video/"+video.getUrl()+video.getExtension();
// Pass the progressBar here. You might have to set it as a final variable.
DownloadFileFromURL downloadFileFromURL = new DownloadFileFromURL(SyncUtil.VIDEOFILE, holder.progressBar);
downloadFileFromURL.execute(url,video.getTitle(),video.getDescription());
}
});
holder.bind(video,listener);
}
Now modify your constructor of the AsyncTask like this.
public DownloadFileFromURL(... , ProgressBar mProgressbar) {
this.mProgressbar = mProgressbar;
this.mProgressbar.setProgress(0);
this.mProgressbar.setMax(100);
}
Add onProgressUpdate in your AsyncTask
protected void onProgressUpdate(Integer... values) {
mProgressbar.setProgress(values[0]);
}
Now in your doInBackground calculate the file size and publish the progress after a certain amount of file is downloaded.
protected void doInBackground() throws IOException {
try {
// Establish connection
URL url = new URL(fileUrl);
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
connection.setDoInput(true);
connection.connect();
final String contentLengthStr = connection.getHeaderField("content-length");
InputStream input = connection.getInputStream();
String data1 = f.getPath();
FileOutputStream stream = new FileOutputStream(data1);
byte data[] = new byte[4096];
int count;
int progressCount = 0;
while ((count = input.read(data)) != -1) {
stream.write(data, 0, count);
progressCount = progressCount + count;
int progress = (int) (((progressCount * 1.0f) / Integer.parseInt(contentLengthStr)) * 10000);
// Publish your progress here
publishProgress(progress);
}
stream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Note:
Passing the original reference of your views is not a very good solution. I would rather set a BroadcastReceiver in my activity and would publish a broadcast with the specific item position in the publishProgress function. So that when the broadcast is received in the main activity, I could call notifyDatasetChanged to take progress effect in the list.

JPG vs WebP performance on Android

I´m trying to get performance stats about how Android load, decode and render WebP images against JPG, but my results are a little confuse.
Decoding WebP images to Bitmap are slow than JPG.
Some stats:
WebP 66% less file size than JPG, 267% more time to decode.
WebP 38% less file size than JPG, 258% more time to decode.
WebP 89% less file size than JPG, 319% more time to decode.
Has someone know about any issue on performance, or why WebP decoding is harder than JPG.
This is my test:
public class BulkLoadFromDisk implements Runnable {
private static final String TAG = "BulkLoadFromDisk";
private static final int TIMES = 10;
private final ResourceProvider resourceProvider;
private final Activity context;
private final int counter;
private long averageLoadTimeNano;
private long averageConvertTimeNano;
private final ImagesFactory.FORMAT format;
private final CompleteListener listener;
public BulkLoadFromDisk(Activity context, ResourceProvider resourceProvider,
CompleteListener listener, ImagesFactory.FORMAT format) {
this.resourceProvider = resourceProvider;
this.context = context;
this.counter = resourceProvider.length();
this.format = format;
this.listener = listener;
}
#Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Log.e(TAG, e.getMessage(), e);
}
try {
String file;
long loadBegin, loadEnd;
long convertBegin, convertEnd;
Bitmap bitmap; Drawable d;
String extension = "." + format.name().toLowerCase();
InputStream inputStream;
for(int j = 0; j < TIMES; j++) {
for(int index = 0; index < counter; index++) {
file = resourceProvider.get(index).concat(extension);
inputStream = context.getAssets().open(file);
// Load bitmap from file
loadBegin = System.nanoTime();
bitmap = BitmapFactory.decodeStream(inputStream);
assert (bitmap != null);
loadEnd = System.nanoTime();
// Convert bitmap to drawable
convertBegin = System.nanoTime();
d = new BitmapDrawable(context.getResources(), bitmap);
assert (d != null);
convertEnd = System.nanoTime();
averageLoadTimeNano += (loadEnd - loadBegin);
averageConvertTimeNano += (convertEnd - convertBegin);
}
}
averageLoadTimeNano = averageLoadTimeNano / (TIMES * counter);
averageConvertTimeNano = averageConvertTimeNano / (TIMES * counter);
if(listener != null && context != null) {
context.runOnUiThread(new Runnable() {
#Override
public void run() {
listener.onComplete(BulkLoadFromDisk.this);
}
});
}
}
catch (final IOException e) {
if(listener != null && context!= null) {
context.runOnUiThread(new Runnable() {
#Override
public void run() {
listener.onError(e);
}
});
}
} finally {
System.gc();
}
}
public interface CompleteListener {
void onComplete(BulkLoadFromDisk task);
void onError(Exception e);
}
public long getAverageLoadTimeNano() {
return averageLoadTimeNano;
}
public long getAverageConvertTimeNano() {
return averageConvertTimeNano;
}
public ImagesFactory.FORMAT getFormat() {
return format;
}
public String resultToString() {
final StringBuffer sb = new StringBuffer("BulkLoadFromDisk{");
sb.append("averageLoadTimeNano=").append(Utils.nanosToBest(averageLoadTimeNano).first
+ Utils.nanosToBest(averageLoadTimeNano).second);
sb.append(", averageConvertTimeNano=").append(Utils.nanosToBest(averageConvertTimeNano).first
+ Utils.nanosToBest(averageConvertTimeNano).second);
sb.append(", format=").append(format);
sb.append('}');
return sb.toString();
}
I know this is an old question and i haven't studied the in-depths of WebP yet, but it's probably because it's a more complex algorith, hence why it has better compression ratios than JPEG. WebP is based on the VP8 codec, which is itself an royalty-free competitor to the widely-used, and heavy, h264 format.
JPEG is widely used, however, it's a really old format and is considerably simpler than the VP8 codec of WebP.

The right way to load a PDF into an ImageView

I'm looking for the proper way to load a PDF File into an ImageView.
I use the BitmapWorkerTask class from Android.
I have a button which allow me to choose the File I want to upload into the ImageView. When I click on this file, the process begin.
My issue is that my PDF is perfectly load 3 time 5. And I don't understand why it's not working the other time.
ImageView map;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
map = (ImageView)findViewById(R.id.pdf);
}
/**
* Use this to load a pdf file from your assets and render it to a Bitmap.
*
* #param context
* current context.
* #param filePath
* of the pdf file in the assets.
* #return a bitmap.
*/
#Nullable
public static Bitmap renderToBitmap(Context context, String filePath) {
Bitmap bi = null;
InputStream inStream = null;
try {
inStream = new FileInputStream(filePath);
bi = renderToBitmap(context, inStream);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inStream.close();
} catch (IOException e) {
// do nothing because the stream has already been closed
}
}
return bi;
}
/**
* Use this to render a pdf file given as InputStream to a Bitmap.
*
* #param context
* current context.
* #param inStream
* the inputStream of the pdf file.
* #return a bitmap.
* #see https://github.com/jblough/Android-Pdf-Viewer-Library/
*/
#Nullable
public static Bitmap renderToBitmap(Context context, InputStream inStream) {
Bitmap bi = null;
try {
byte[] decode = IOUtils.toByteArray(inStream);
ByteBuffer buf = ByteBuffer.wrap(decode);
PDFPage mPdfPage = new PDFFile(buf).getPage(0);
float width = mPdfPage.getWidth();
float height = mPdfPage.getHeight();
RectF clip = null;
bi = mPdfPage.getImage((int) (width), (int) (height), clip, true,
true);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inStream.close();
} catch (IOException e) {
// do nothing because the stream has already been closed
}
}
return bi;
}
private void renderMap() {
String mapFilePath = Environment.getExternalStorageDirectory().toString()+"/Android/data/com.empower.data/"+mapFileName;
BitmapWorkerTask task = new BitmapWorkerTask(map, mapFilePath, MainActivity.this);
task.loadBitmap(R.id.pdf, map, mapFilePath);
map.setVisibility(View.VISIBLE);
map.invalidate();
}
And my BitmapWorkerTask.java
public class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
private String mapFilePath1;
private Context context;
private RelativeLayout loadingPanel;
public BitmapWorkerTask(ImageView imageView, String mapFilePath2, Context context) {
// Use a WeakReference to ensure the ImageView can be garbage collected
this.context = context;
mapFilePath1 = mapFilePath2;
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
#Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
Bitmap bm = MainActivity.renderToBitmap(context , mapFilePath1);
return bm;
}
#Override
protected void onPreExecute() {
loadingPanel = (RelativeLayout)((Activity) context).findViewById(R.id.loadingPanel);
loadingPanel.setVisibility(View.VISIBLE);
}
// Once complete, see if ImageView is still around and set bitmap.
#Override
protected void onPostExecute(final Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
imageView.setImageBitmap(bitmap);
loadingPanel.setVisibility(View.GONE);
}
}, 500);
}
}
}
public void loadBitmap(int resId, ImageView imageView, String mapFilePath1) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView, mapFilePath1, context);
task.execute(resId);
}
}

AudioRecord producing gaps of zeroes on Android 5.01

Using AudioRecord, I have attempted to write a test app to record a couple of seconds of audio to be displayed to the screen. However, I seem to get a repeating pattern of zero value regions as shown below. I'm not sure if this is normal behaviour or an error in my code.
MainActivity.java
public class MainActivity extends Activity implements OnClickListener
{
private static final int SAMPLE_RATE = 44100;
private Button recordButton, playButton;
private String filePath;
private boolean recording;
private AudioRecord record;
private short[] data;
private TestView testView;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button recordButton = (Button) this.findViewById(R.id.recordButton);
recordButton.setOnClickListener(this);
Button playButton = (Button)findViewById(R.id.playButton);
playButton.setOnClickListener(this);
FrameLayout frame = (FrameLayout)findViewById(R.id.myFrame);
frame.addView(testView = new TestView(this));
}
#Override
public void onClick(View v)
{
if(v.getId() == R.id.recordButton)
{
if(!recording)
{
int bufferSize = AudioRecord.getMinBufferSize( SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
record = new AudioRecord( MediaRecorder.AudioSource.MIC,
SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize * 2);
data = new short[10 * SAMPLE_RATE]; // Records up to 10 seconds
new Thread()
{
#Override
public void run()
{
recordAudio();
}
}.start();
recording = true;
Toast.makeText(this, "recording...", Toast.LENGTH_SHORT).show();
}
else
{
recording = false;
Toast.makeText(this, "finished", Toast.LENGTH_SHORT).show();
}
}
else if(v.getId() == R.id.playButton)
{
testView.invalidate();
Toast.makeText(this, "play/pause", Toast.LENGTH_SHORT).show();
}
}
void recordAudio()
{
record.startRecording();
int index = 0;
while(recording)
{
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int result = record.read(data, index, SAMPLE_RATE); // read 1 second at a time
if(result == AudioRecord.ERROR_INVALID_OPERATION || result == AudioRecord.ERROR_BAD_VALUE)
{
App.d("SOME SORT OF RECORDING ERROR MATE");
return;
}
else
{
index += result; // increment by number of bytes read
App.d("read: "+result);
}
}
record.stop();
data = Arrays.copyOf(data, index);
testView.setData(data);
}
#Override
protected void onPause()
{
super.onPause();
}
}
TestView.java
public class TestView extends View
{
private short[] data;
Paint paint = new Paint();
Path path = new Path();
float min, max;
public TestView(Context context)
{
super(context);
paint.setColor(Color.BLACK);
paint.setStrokeWidth(1);
paint.setStyle(Style.FILL_AND_STROKE);
}
void setData(short[] data)
{
min = Short.MAX_VALUE;
max = Short.MIN_VALUE;
this.data = data;
for(int i = 0; i < data.length; i++)
{
if(data[i] < min)
min = data[i];
if(data[i] > max)
max = data[i];
}
}
#Override
protected void onDraw(Canvas canvas)
{
canvas.drawRGB(255, 255, 255);
if(data != null)
{
float interval = (float)this.getWidth()/data.length;
for(int i = 0; i < data.length; i+=10)
canvas.drawCircle(i*interval,(data[i]-min)/(max - min)*this.getHeight(),5 ,paint);
}
super.onDraw(canvas);
}
}
Your navigation bar icons make it look like you are probably running on Android 5, and there is a bug in the Android 5.0 release which can cause precisely the problem you are seeing.
Recording to shorts gave an erroneous return value on the L preview, and while substantially reworking the code in the course of fixing that they mistakenly doubled the offset argument in the 5.0 release. Your code increments the index by the (correct) amount it has read in each call, but a pointer math mistake in the audio internals will double the offset you pass, meaning that each period of recording ends up followed by an equal period of unwritten-to buffer, which you see as those gaps of zeroes.
The issue was reported at http://code.google.com/p/android/issues/detail?id=80866
A patch submitted at that time last fall was declined as they said they had already dealt with it internally. Looking at the git history for AOSP 5.1, that would appear to have been internal commit 283a9d9e1 of November 13, which was not yet public when I encountered it later that month. While I haven't tried this on 5.1 yet, it seems like that should fix it, so most likely it is broken from 5.0-5.02 (and in a different way on the L preview) but works correctly with 4.4 and earlier, as well as with 5.1 and later.
The simplest workaround for consistent behavior across broken and unbroken release versions is to avoid ever passing a non-zero offset when recording shorts - that's how I fixed the program where I encountered the problem. A more complicated idea would be to try to figure out if you are on a broken version, and if so halve the passed argument. One method would be to detect the device version, but it's conceivable some vendor or custom ROM 5.0 builds might have been patched, so you could go a step further and do a short recording with a test offset to a zeroed buffer, then scan it to see where the non-zero data actually starts.
Do not pass half the offset to the read-function as suggested in the accepted answer. The offset is an integer and might be an uneven number. This will result in poor audio quality and would be incompatible to android versions other than 5.0.1. and 5.0.2. I used the following work-around, which works for all android versions. I changed:
short[] buffer = new short[frame_size*(frame_rate)];
num = record.read(buffer, offset, frame_size);
into
short[] buffer = new short[frame_size*(frame_rate)];
short[] buffer_bugfix = new short[frame_size];
num = record.read(buffer_bugfix, 0, frame_size);
System.arraycopy(buffer_bugfix, 0, buffer, offset, frame_size);
In words instead of letting the read-function copy the data to the offset position of the large buffer, I let the read-function copy the data to the smaller buffer. I then insert this data manually to the offset position of the large buffer.
I can't check right now your code but I can provide you with some sample code you can test:
private static int channel_config = AudioFormat.CHANNEL_IN_MONO;
private static int format = AudioFormat.ENCODING_PCM_16BIT;
private static int Fs = 16000;
private static int minBufferSize;
private boolean isRecording;
private boolean isProcessing;
private boolean isNewAudioFragment;
private final static int bytesPerSample = 2; // As it is 16bit PCM
private final double amplification = 1.0; // choose a number as you like
private static int frameLength = 512; // number of samples per frame => 32[ms] #Fs = 16[KHz]
private static int windowLength = 16; // number of frames per window => 512[ms] #Fs = 16[KHz]
private static int maxBufferedWindows = 8; // number of buffered windows => 4096 [ms] #Fs = 16[KHz]
private static int bufferSize = frameLength*bytesPerSample;
private static double[] hannWindow = new double[frameLength*bytesPerSample];
private Queue<byte[]> queue = new LinkedList<byte[]>();
private Semaphore semaphoreProcess = new Semaphore(0, true);
private RecordSignal recordSignalThread;
private ProcessSignal processSignalThread;
public static class RecorderSingleton {
public static RecorderSingleton instance = new RecorderSingleton();
private AudioRecord recordInstance = null;
private RecorderSingleton() {
minBufferSize = AudioRecord.getMinBufferSize(Fs, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
while(minBufferSize>bufferSize) {
bufferSize = bufferSize*2;
}
}
public boolean init() {
recordInstance = new AudioRecord(MediaRecorder.AudioSource.MIC, Fs, channel_config, format, bufferSize);
if (recordInstance.getState() != AudioRecord.STATE_INITIALIZED) {
Log.d("audiotestActivity", "Fail to initialize AudioRecord object");
Log.d("audiotestActivity", "AudioRecord.getState()=" + recordInstance.getState());
}
if (recordInstance.getState() == AudioRecord.STATE_UNINITIALIZED) {
return false;
}
return true;
}
public int getBufferSize() {return bufferSize;}
public boolean start() {
if (recordInstance != null && recordInstance.getState() != AudioRecord.STATE_UNINITIALIZED) {
if (recordInstance.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
recordInstance.stop();
}
recordInstance.release();
}
if (!init()) {
return false;
}
recordInstance.startRecording();
return true;
}
public int read(byte[] audioBuffer) {
if (recordInstance == null) {
return AudioRecord.ERROR_INVALID_OPERATION;
}
int ret = recordInstance.read(audioBuffer, 0, bufferSize);
return ret;
}
public void stop() {
if (recordInstance == null) {
return;
}
if(recordInstance.getState()==AudioRecord.STATE_UNINITIALIZED) {
Log.d("AudioTest", "instance uninitialized");
return;
}
if(recordInstance.getState()==AudioRecord.STATE_INITIALIZED) {
recordInstance.stop();
recordInstance.release();
}
}
}
public class RecordSignal implements Runnable {
private boolean cancelled = false;
public void run() {
Looper.prepare();
// We're important...android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
int bufferRead = 0;
byte[] inAudioBuffer;
if (!RecorderSingleton.instance.start()) {
return;
}
try {
Log.d("audiotestActivity", "Recorder Started");
while(isRecording) {
inAudioBuffer = null;
inAudioBuffer = new byte[bufferSize];
bufferRead = RecorderSingleton.instance.read(inAudioBuffer);
if (bufferRead == AudioRecord.ERROR_INVALID_OPERATION) {
throw new IllegalStateException("read() returned AudioRecord.ERROR_INVALID_OPERATION");
} else if (bufferRead == AudioRecord.ERROR_BAD_VALUE) {
throw new IllegalStateException("read() returned AudioRecord.ERROR_BAD_VALUE");
}
queue.add(inAudioBuffer);
semaphoreProcess.release();
}
}
finally {
// Close resources...
stop();
}
Looper.loop();
}
public void stop() {
RecorderSingleton.instance.stop();
}
public void cancel() {
setCancelled(true);
}
public boolean isCancelled() {
return cancelled;
}
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}
public class ProcessSignal implements Runnable {
public void run() {
Looper.prepare();
//android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DEFAULT);
while(isProcessing) {
try {
semaphoreProcess.acquire();
byte[] outAudioBuffer = new byte[frameLength*bytesPerSample*(bufferSize/(frameLength*bytesPerSample))];
outAudioBuffer = queue.element();
if(queue.size()>0) {
// do something, process your samples
}
queue.poll();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
Looper.loop();
}
}
and to start and stop simply:
public void startAudioTest() {
if(recordSignalThread!=null) {
recordSignalThread.stop();
recordSignalThread.cancel();
recordSignalThread = null;
}
if(processSignalThread!=null) {
processSignalThread = null;
}
recordSignalThread = new RecordSignal();
processSignalThread = new ProcessSignal();
new Thread(recordSignalThread).start();
new Thread(processSignalThread).start();
isRecording = true;
isProcessing = true;
}
public void stopAudioTest() {
isRecording = false;
isProcessing = false;
if(processSignalThread!=null) {
processSignalThread = null;
}
if(recordSignalThread!=null) {
recordSignalThread.cancel();
recordSignalThread = null;
}
}

OutOfMemoryError bitmaps android

I am caching my images in android and not sure how to reuse bitmaps as android suggest here:
https://developer.android.com/training/displaying-bitmaps/manage-memory.html
here is my code
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
this.imageCache= new LruCache<String, Bitmap>(cacheSize){
#Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
this.m_adapter = new ImageScreenAdapter(this, R.layout.imagelist_item, items, imageCache);
setListAdapter(this.m_adapter);
this is the method I use to download my bitmaps
private Bitmap downloadBitmap(String url, ProgressBar progress, int position) {
final AndroidHttpClient client = AndroidHttpClient.newInstance("Android");
final HttpGet getRequest = new HttpGet(url);
try {
HttpResponse response = client.execute(getRequest);
final int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
Log.w("ImageDownloader", "Error " + statusCode
+ " while retrieving bitmap from " + url);
if(progress!=null)
{
RemoveImageResults(position);
return null;
}
return BitmapFactory.decodeResource(getResources(), R.drawable.missingpic);
}
final HttpEntity entity = response.getEntity();
if (entity != null) {
InputStream inputStream = null;
try {
inputStream = entity.getContent();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inDither = true;
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream,null, options);
return bitmap;
} finally {
if (inputStream != null) {
inputStream.close();
}
entity.consumeContent();
}
}
} catch (Exception e) {
// Could provide a more explicit error message for IOException or
// IllegalStateException
getRequest.abort();
//Log.w("ImageDownloader", "Error while retrieving bitmap from " + url);
} finally {
if (client != null) {
client.close();
}
}
return null;
}
and in my AsyncTask
#Override
protected void onPostExecute(Bitmap result) {
final Bitmap Image=result;
if(Image!=null)
imageCache.put(imageUrl, Image);
myActivity.runOnUiThread(new Runnable() {
public void run() {
imageView.setImageBitmap(Image);
imageView.setVisibility(View.VISIBLE);
}
});
}
private Bitmap download_Image(String url) {
return downloadBitmap(url, progress, position);
}
But this could run out of memory if it gets up to 1000 images in my list adapter, so how do I reuse the bitmaps or recycle the unused ones? I am targeting android 3.0 or better and as android suggest I could use a Set> mReusableBitmaps; but I don't follow how to implement this.
The best way to handle this issue is to implement some kind of lazy image loading where you have weak referenced bitmaps that can be recycled by the system easier. There is a ton of samples online and there is a very popular open source library on github that does all this for you. They even have callbacks that you can use to display a progress bar while your image loads and get rid of it when the image is done downloading. You can find it here : https://github.com/nostra13/Android-Universal-Image-Loader

Categories

Resources