/* * This file provided by Facebook is for non-commercial testing and evaluation * purposes only. Facebook reserves all rights not expressly granted. * * 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 * FACEBOOK 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. */ package com.facebook.samples.comparison; import android.content.Context; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Point; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Debug; import android.os.Handler; import android.os.Looper; import android.provider.MediaStore; import android.support.v7.app.ActionBarActivity; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.Display; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.Spinner; import android.widget.TextView; import com.facebook.common.internal.VisibleForTesting; import com.facebook.common.logging.FLog; import com.facebook.samples.comparison.adapters.AQueryAdapter; import com.facebook.samples.comparison.adapters.FrescoAdapter; import com.facebook.samples.comparison.adapters.GlideAdapter; import com.facebook.samples.comparison.adapters.ImageListAdapter; import com.facebook.samples.comparison.adapters.PicassoAdapter; import com.facebook.samples.comparison.adapters.UilAdapter; import com.facebook.samples.comparison.adapters.VolleyAdapter; import com.facebook.samples.comparison.adapters.VolleyDraweeAdapter; import com.facebook.samples.comparison.configs.imagepipeline.ImagePipelineConfigFactory; import com.facebook.samples.comparison.instrumentation.PerfListener; import com.facebook.samples.comparison.urlsfetcher.ImageFormat; import com.facebook.samples.comparison.urlsfetcher.ImageSize; import com.facebook.samples.comparison.urlsfetcher.ImageUrlsFetcher; import com.facebook.samples.comparison.urlsfetcher.ImageUrlsRequestBuilder; import java.util.ArrayList; import java.util.List; import java.util.Locale; public class MainActivity extends ActionBarActivity { private static final String TAG = "FrescoSample"; // These need to be in sync with {@link R.array.image_loaders} public static final int FRESCO_INDEX = 1; public static final int FRESCO_OKHTTP_INDEX = 2; public static final int GLIDE_INDEX = 3; public static final int PICASSO_INDEX = 4; public static final int UIL_INDEX = 5; public static final int VOLLEY_INDEX = 6; public static final int AQUERY_INDEX = 7; // These need to be in sync with {@link R.array.image_sources} public static final int NETWORK_INDEX = 1; public static final int LOCAL_INDEX = 2; private static final int COLS_NUMBER = 3; private static final long STATS_CLOCK_INTERVAL_MS = 1000; private static final int DEFAULT_MESSAGE_SIZE = 1024; private static final int BYTES_IN_MEGABYTE = 1024 * 1024; private static final String EXTRA_ALLOW_ANIMATIONS = "allow_animations"; private static final String EXTRA_USE_DRAWEE = "use_drawee"; private static final String EXTRA_CURRENT_ADAPTER_INDEX = "current_adapter_index"; private static final String EXTRA_CURRENT_SOURCE_ADAPTER_INDEX = "current_source_adapter_index"; private Handler mHandler; private Runnable mStatsClockTickRunnable; private TextView mStatsDisplay; private Spinner mLoaderSelect; private Spinner mSourceSelect; private boolean mUseDrawee; private boolean mAllowAnimations; private int mCurrentLoaderAdapterIndex; private int mCurrentSourceAdapterIndex; private ImageListAdapter mCurrentAdapter; private RecyclerView mRecyclerView; private PerfListener mPerfListener; private List<String> mImageUrls = new ArrayList<>(); private boolean mUrlsLocal = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRecyclerView = (RecyclerView) findViewById(R.id.image_grid); mRecyclerView.setLayoutManager(new GridLayoutManager(this, COLS_NUMBER)); FLog.setMinimumLoggingLevel(FLog.WARN); Drawables.init(getResources()); mPerfListener = new PerfListener(); mAllowAnimations = true; mUseDrawee = true; mCurrentLoaderAdapterIndex = 0; mCurrentSourceAdapterIndex = 0; if (savedInstanceState != null) { mAllowAnimations = savedInstanceState.getBoolean(EXTRA_ALLOW_ANIMATIONS); mUseDrawee = savedInstanceState.getBoolean(EXTRA_USE_DRAWEE); mCurrentLoaderAdapterIndex = savedInstanceState.getInt(EXTRA_CURRENT_ADAPTER_INDEX); mCurrentSourceAdapterIndex = savedInstanceState.getInt(EXTRA_CURRENT_SOURCE_ADAPTER_INDEX); } mHandler = new Handler(Looper.getMainLooper()); mStatsClockTickRunnable = new Runnable() { @Override public void run() { updateStats(); scheduleNextStatsClockTick(); } }; mCurrentAdapter = null; mStatsDisplay = (TextView) findViewById(R.id.stats_display); mLoaderSelect = (Spinner) findViewById(R.id.loader_select); mLoaderSelect.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { setLoaderAdapter(position); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); mLoaderSelect.setSelection(mCurrentLoaderAdapterIndex); mSourceSelect = (Spinner) findViewById(R.id.source_select); mSourceSelect.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { setSourceAdapter(position); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); mSourceSelect.setSelection(mCurrentSourceAdapterIndex); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(EXTRA_ALLOW_ANIMATIONS, mAllowAnimations); outState.putBoolean(EXTRA_USE_DRAWEE, mUseDrawee); outState.putInt(EXTRA_CURRENT_ADAPTER_INDEX, mCurrentLoaderAdapterIndex); outState.putInt(EXTRA_CURRENT_SOURCE_ADAPTER_INDEX, mCurrentSourceAdapterIndex); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.findItem(R.id.allow_animations).setChecked(mAllowAnimations); menu.findItem(R.id.use_drawee).setChecked(mUseDrawee); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.allow_animations) { setAllowAnimations(!item.isChecked()); return true; } if (id == R.id.use_drawee) { setUseDrawee(!item.isChecked()); return true; } return super.onOptionsItemSelected(item); } @Override protected void onStart() { super.onStart(); updateStats(); scheduleNextStatsClockTick(); } protected void onStop() { super.onStop(); cancelNextStatsClockTick(); } @VisibleForTesting public void setAllowAnimations(boolean allowAnimations) { mAllowAnimations = allowAnimations; supportInvalidateOptionsMenu(); updateAdapter(null); loadUrls(); } @VisibleForTesting public void setUseDrawee(boolean useDrawee) { mUseDrawee = useDrawee; supportInvalidateOptionsMenu(); setLoaderAdapter(mCurrentLoaderAdapterIndex); setSourceAdapter(mCurrentSourceAdapterIndex); } private void resetAdapter() { if (mCurrentAdapter != null) { mCurrentAdapter.shutDown(); mCurrentAdapter = null; System.gc(); } } private void setLoaderAdapter(int index) { FLog.v(TAG, "onImageLoaderSelect: %d", index); resetAdapter(); mCurrentLoaderAdapterIndex = index; mPerfListener = new PerfListener(); switch (index) { case FRESCO_INDEX: case FRESCO_OKHTTP_INDEX: mCurrentAdapter = new FrescoAdapter( this, mPerfListener, index == FRESCO_INDEX ? ImagePipelineConfigFactory.getImagePipelineConfig(this) : ImagePipelineConfigFactory.getOkHttpImagePipelineConfig(this)); break; case GLIDE_INDEX: mCurrentAdapter = new GlideAdapter(this, mPerfListener); break; case PICASSO_INDEX: mCurrentAdapter = new PicassoAdapter(this, mPerfListener); break; case UIL_INDEX: mCurrentAdapter = new UilAdapter(this, mPerfListener); break; case VOLLEY_INDEX: mCurrentAdapter = mUseDrawee ? new VolleyDraweeAdapter(this, mPerfListener) : new VolleyAdapter(this, mPerfListener); break; case AQUERY_INDEX: mCurrentAdapter = new AQueryAdapter(this, mPerfListener); break; default: mCurrentAdapter = null; return; } mRecyclerView.setAdapter(mCurrentAdapter); updateAdapter(mImageUrls); updateStats(); } private void setSourceAdapter(int index) { FLog.v(TAG, "onImageSourceSelect: %d", index); mCurrentSourceAdapterIndex = index; switch (index) { case NETWORK_INDEX: mUrlsLocal = false; break; case LOCAL_INDEX: mUrlsLocal = true; break; default: resetAdapter(); mImageUrls.clear(); return; } loadUrls(); setLoaderAdapter(mCurrentLoaderAdapterIndex); } private void loadUrls() { if (mUrlsLocal) { loadLocalUrls(); } else { loadNetworkUrls(); } } private void scheduleNextStatsClockTick() { mHandler.postDelayed(mStatsClockTickRunnable, STATS_CLOCK_INTERVAL_MS); } private void cancelNextStatsClockTick() { mHandler.removeCallbacks(mStatsClockTickRunnable); } public static int calcDesiredSize(Context context, int parentWidth, int parentHeight) { int orientation = context.getResources().getConfiguration().orientation; int desiredSize = (orientation == Configuration.ORIENTATION_LANDSCAPE) ? parentHeight / 2 : parentHeight / 3; return Math.min(desiredSize, parentWidth); } private ImageSize chooseImageSize() { ViewGroup.LayoutParams layoutParams = mRecyclerView.getLayoutParams(); if (layoutParams == null) { return ImageSize.LARGE_THUMBNAIL; } int size = calcDesiredSize(this, layoutParams.width, layoutParams.height); if (size <= 90) { return ImageSize.SMALL_SQUARE; } else if (size <= 160) { return ImageSize.SMALL_THUMBNAIL; } else if (size <= 320) { return ImageSize.MEDIUM_THUMBNAIL; } else if (size <= 640) { return ImageSize.LARGE_THUMBNAIL; } else if (size <= 1024) { return ImageSize.HUGE_THUMBNAIL; } else { return ImageSize.ORIGINAL_IMAGE; } } private void loadNetworkUrls() { String url = "https://api.imgur.com/3/gallery/hot/viral/0.json"; ImageSize staticSize = chooseImageSize(); ImageUrlsRequestBuilder builder = new ImageUrlsRequestBuilder(url) .addImageFormat(ImageFormat.JPEG, staticSize) .addImageFormat(ImageFormat.PNG, staticSize); if (mAllowAnimations) { builder.addImageFormat( ImageFormat.GIF, ImageSize.ORIGINAL_IMAGE); } ImageUrlsFetcher.getImageUrls( builder.build(), new ImageUrlsFetcher.Callback() { @Override public void onFinish(List<String> result) { // If the user changes to local images before the call comes back, then this should // be ignored if (!mUrlsLocal) { mImageUrls = result; updateAdapter(mImageUrls); } } }); } private void loadLocalUrls() { Uri externalContentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; String[] projection = {MediaStore.Images.Media._ID}; Cursor cursor = null; try { cursor = getContentResolver().query(externalContentUri, projection, null, null, null); mImageUrls.clear(); int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID); String imageId; Uri imageUri; while (cursor.moveToNext()) { imageId = cursor.getString(columnIndex); imageUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageId); mImageUrls.add(imageUri.toString()); } } finally { if (cursor != null) { cursor.close(); } } } private void updateAdapter(List<String> urls) { if (mCurrentAdapter != null) { mCurrentAdapter.clear(); if (urls != null) { for (String url : urls) { mCurrentAdapter.addUrl(url); } } mCurrentAdapter.notifyDataSetChanged(); } } private void updateStats() { final Runtime runtime = Runtime.getRuntime(); final long heapMemory = runtime.totalMemory() - runtime.freeMemory(); final StringBuilder sb = new StringBuilder(DEFAULT_MESSAGE_SIZE); // When changing format of output below, make sure to sync "run_comparison.py" as well sb.append("Heap: "); appendSize(sb, heapMemory); sb.append(" Java "); appendSize(sb, Debug.getNativeHeapSize()); sb.append(" native\n"); appendTime(sb, "Avg wait time: ", mPerfListener.getAverageWaitTime(), "\n"); appendNumber(sb, "Requests: ", mPerfListener.getOutstandingRequests(), " outsdng "); appendNumber(sb, "", mPerfListener.getCancelledRequests(), " cncld\n"); final String message = sb.toString(); mStatsDisplay.setText(message); FLog.i(TAG, message); } private static void appendSize(StringBuilder sb, long bytes) { String value = String.format(Locale.getDefault(), "%.1f MB", (float) bytes / BYTES_IN_MEGABYTE); sb.append(value); } private static void appendTime(StringBuilder sb, String prefix, long timeMs, String suffix) { appendValue(sb, prefix, timeMs + " ms", suffix); } private static void appendNumber(StringBuilder sb, String prefix, long number, String suffix) { appendValue(sb, prefix, number + "", suffix); } private static void appendValue(StringBuilder sb, String prefix, String value, String suffix) { sb.append(prefix).append(value).append(suffix); } /** * Determines display's height. */ public int getDisplayHeight() { Display display = getWindowManager().getDefaultDisplay(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR2) { return display.getHeight(); } else { final Point size = new Point(); display.getSize(size); return size.y; } } }