2

I am displaying all apps installed in a gridView. When loading a lot of apps, lets say 30 or more, the icons will display at the default Android icon and then several seconds later update to the correct icon. I am wondering about improvements I can make to my code to make the icon images display faster.

Load the following with: new LoadIconsTask().execute(mApps.toArray(new AppsInstalled[]{}));

Here is what I do.

private class LoadIconsTask extends AsyncTask<AppsInstalled, Void, Void>{

        @Override
        protected Void doInBackground(AppsInstalled... params) {
            // TODO Auto-generated method stub
            Map<String, Drawable> icons = new HashMap<String, Drawable>();
            PackageManager manager = getApplicationContext().getPackageManager();

            // match package name with icon, set Adapter with loaded Map
            for (AppsInstalled app : params) {
                String pkgName = app.getAppUniqueId();
                Drawable ico = null;
                try {
                    Intent i = manager.getLaunchIntentForPackage(pkgName);
                    if (i != null) {
                        ico = manager.getActivityIcon(i);
                    }               
                } catch (NameNotFoundException e) {
                    Log.e(TAG, "Unable to find icon match based on package: " + pkgName 
                            + " : " + e.getMessage());
                }
                icons.put(app.getAppUniqueId(), ico);
            }
            mAdapter.setIcons(icons);
            return null;
        }

Also populate my listing of apps before I loadIconsTask() with

private List<App> loadInstalledApps(boolean includeSysApps) {
    List<App> apps = new ArrayList<App>();

    // the package manager contains the information about all installed apps
    PackageManager packageManager = getPackageManager();

    List<PackageInfo> packs = packageManager.getInstalledPackages(0); // PackageManager.GET_META_DATA

    for (int i = 0; i < packs.size(); i++) {
        PackageInfo p = packs.get(i);
        ApplicationInfo a = p.applicationInfo;
        // skip system apps if they shall not be included
        if ((!includeSysApps)
                && ((a.flags & ApplicationInfo.FLAG_SYSTEM) == 1)) {
            continue;
        }
        App app = new App();
        app.setTitle(p.applicationInfo.loadLabel(packageManager).toString());
        app.setPackageName(p.packageName);
        app.setVersionName(p.versionName);
        app.setVersionCode(p.versionCode);
        CharSequence description = p.applicationInfo
                .loadDescription(packageManager);
        app.setDescription(description != null ? description.toString()
                : "");
        apps.add(app);
    }
    return apps;
}

In regards to my Adapter class it is standard. My getView() looks like the following:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        AppViewHolder holder;
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.row, null);

            // creates a ViewHolder and stores a reference to the children view
            // we want to bind data to
            holder = new AppViewHolder();
            holder.mTitle = (TextView) convertView.findViewById(R.id.apptitle);
            holder.mIcon = (ImageView) convertView.findViewById(R.id.appicon);
            convertView.setTag(holder);
        } else {
            // reuse/overwrite the view passed assuming that it is castable!
            holder = (AppViewHolder) convertView.getTag();
        }

        App app = mApps.get(position);

        holder.setTitle(app.getTitle());
        if (mIcons == null || mIcons.get(app.getPackageName()) == null) {
            holder.setIcon(mStdImg);
        } else {
            holder.setIcon(mIcons.get(app.getPackageName()));
        }

        return convertView;
    }

Is there a better way? Can I somehow store the images of the icons in a data structure and when I return back to this Activity I can skip the loadIconsTask? Is that possible? Thank you in advance.

portfoliobuilder
  • 7,556
  • 14
  • 76
  • 136

3 Answers3

12

You can use Picasso library with a custom RequestHandler to load the icons in the background.

  1. First create a RequestHandler which will handle the specific case where an app icon needs to be loaded.

    public class AppIconRequestHandler extends RequestHandler {
    
        /** Uri scheme for app icons */
        public static final String SCHEME_APP_ICON = "app-icon";
    
        private PackageManager mPackageManager;
    
        public AppIconRequestHandler(Context context) {
            mPackageManager = context.getPackageManager();
        }
    
        /**
         * Create an Uri that can be handled by this RequestHandler based on the package name
         */
        public static Uri getUri(String packageName) {
            return Uri.fromParts(SCHEME_APP_ICON, packageName, null);
        }
    
        @Override
        public boolean canHandleRequest(Request data) {
            // only handle Uris matching our scheme
            return (SCHEME_APP_ICON.equals(data.uri.getScheme()));
        }
    
        @Override
        public Result load(Request request, int networkPolicy) throws IOException {
            String packageName = request.uri.getSchemeSpecificPart();
            Drawable drawable;
            try {
                drawable = mPackageManager.getApplicationIcon(packageName);
            } catch (PackageManager.NameNotFoundException ignored) {
                return null;
            }
    
            Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            return new Result(bitmap, Picasso.LoadedFrom.DISK);
        }
    }
    
  2. In your adapter, create a Picasso instance and add your RequestHandler.

    // field variable
    private Picasso mPicasso;
    
    // in constructor
    Picasso.Builder builder = new Picasso.Builder(context);
    builder.addRequestHandler(new AppIconRequestHandler(context));
    mPicasso = builder.build();
    
  3. In your adapter's getView() load the icon using Picasso.

    mPicasso.load(AppIconRequestHandler.getUri(app.packageName)).into(holder.mIcon);
    
Benito Bertoli
  • 25,285
  • 12
  • 54
  • 61
  • Works well! I was trying to figure this setup out and failed a few times but as soon as I saw your code above I realized what I was doing wrong. Thanks for the great example! – Codeversed Mar 24 '16 at 14:11
  • 3
    Just some improvements over Benito's answer: https://gist.github.com/guavabot/57da5064e93260ee35a2ef9cfbb6bec2 Added 1. A static method to get an Uri handled by this RequestHander. 2. Return null when icon can't be loaded to avoid NPE in Result's constructor. 3. Better handling of the Uri to avoid that `SCHEME_APP_ICON + ":"` thing. – sorianiv Apr 16 '16 at 10:46
  • Thank you @sorianiv. I've updated my answer with your improvements. – Benito Bertoli Nov 27 '16 at 22:40
  • This request handler doesn't caches bitmap it always queries from packagmanager – Vishnu Prasad Jul 05 '17 at 05:36
6

it's surprising the system takes that much time in getting these lists, you may want to add some logs with timestamping to see which one is the demanding operation.

I don't know if that procedure can be further optimized, I haven't used these system API's very much, but what you can certainly do is to cache this list

  • Create it in onResume / onCreate as a static list, and (for the sake of correctness) destroy it in onPause / onStop if you want to consider the case where the user may install an application while in your app (onPause will be called), but you can certainly skip this step.

  • You may want to also permanently cache the list in the sdcard and find some simple and fast heuristic to decide if the list has changed in order to recreate it. Something like maybe the number of installed packages together with something else (to discard the case when the user uninstalls 3 apps and install 3 different apps, the number of packages will be the same and you have to detect this somehow).

EDIT- To recommend a caching mechanism, you should identify which one is the slow operation. Just guessing, and from your question "the icons take some seconds to appear" it looks like that the slow operation is:

ico = manager.getActivityIcon(i);

but I might be wrong. Let's suppose I'm right, so a cheap caching can be:

1) Move the Map<String, Drawable> icons = new HashMap<String, Drawable>(); outside of doInBackground to the root of the class and make it static, like:

private static Map<String, Drawable> sIcons = new HashMap<String, Drawable>()

2) In your loadIconsTask consider the case you already have this icon:

 for (AppsInstalled app : params) {

                String pkgName = app.getAppUniqueId();
                if (sIcons.containsKey(pkgName) continue;
                .
                .
                .
    }

This is because sIcons is now static and will be alive as long as your application is alive.

3) As a classy thing, you may want to change sIcons from Drawable to Bitmap. Why? Because a Drawable may keep inside references to Views and Context and it's a potential memory leak. You can get the Bitmap from a Drawable very easily, calling drawable.getBitmap() , (Assuming drawable is a BitmapDrawable, but it will obviously be because it's an app icon), so suming up you'll have:

        // the static icon dictionary now stores Bitmaps
        static Map<String, Bitmap> sIcons = new HashMap<String, Bitmap>();
        .
        .
        // we store the bitmap instead of the drawable
        sIcons.put(app.getAppUniqueId(), ((BitmapDrawable)ico).getBitmap());
        .
        .
        // when setting the icon, we create the drawable back
        holder.setIcon(new BitmapDrawable(mIcons.get(app.getPackageName())));

This way your static hashmap will never leak any memory.

4) You may want to check if it's worth to store those bitmaps on disk. Mind this is some additional work and it might not be worth if the time to load the icon from disk is similar to the time to load the icon calling ico = manager.getActivityIcon(i);. It may be (i don't know if manager.getActivityIcon() extracts the icon from the APK) but it certainly may be not.

If you check out it's worth, when you create the list, you can save the bitmaps to the sdcard like this:

  // prepare a file to the application cache dir.
    File cachedFile=new File(context.getCacheDir(), "icon-"+app.getPackageName());
    // save our bitmap as a compressed JPEG with the package name as filename
    myBitmap.compress(CompressFormat.JPEG, quality, new FileOutputStream(cachedFile);

... then when loading the icons, check if the icon exists and load from the sdcard instead:

String key=app.getPackageName();

File localFile=new File(context.getCacheDir(), "icon-"+key);

if (localFile.exists()) {
    // the file exists in the sdcard, just load it
    Bitmap myBitmap = BitmapFactory.decodeStream(new FileInputStream(localFile));

    // we have our bitmap from the sdcard !! Let's put it into our HashMap
    sIcons.put(key, myBitmap)
} else {
    // use the slow method
}

Well as you see it's just a matter of identifying the slow operation. If our above assumption is correct, your stored bitmaps will survive your application destroy and it will hopefully optimize the icon loading.

rupps
  • 9,712
  • 4
  • 55
  • 95
  • Yes, caching is what I had in mind. I just haven't done so before. If you can provide any code samples to get me started that would be nice. But I will keep up my research now that I have the technical term "caching". – portfoliobuilder Nov 12 '14 at 02:49
  • Great explanation! Thank you, I will take these steps. – portfoliobuilder Nov 12 '14 at 17:54
  • 1
    also, try this other alternative thing I just thought of: create the hashmap but do not fill it at the beginning. Rather, In adapter `getView`, check if the `hashmap` already has the icon, if it does, use it, if it doesnt, either a) load it into the hashmap and into the view obtaining it synchronously from `getActivityIcon` or b) do the same using an `AsyncTask` . The difference is **you won't load ALL the installed app icons at once, but only as many as the rows that fit on screen.** This seems very scalable: Time will always be the same regardless of number of apps installed! – rupps Nov 12 '14 at 19:38
  • Wow, that is great idea too. I never thought about that. I will try all of these things. Thank you very much sir. – portfoliobuilder Nov 12 '14 at 20:56
  • If u consider Bitmap suggestion, I tried, but for some application instead of BitmapDrawable you will get AdaptiveIconDrawable, for some reason it's not exposing any method to get desired bitmap. – kolboc Oct 12 '19 at 21:11
  • wow answer was from a long long time ago and Android has evolved :) The problem is now you get that AdaptiveIconDrawable, that instead of a simple bitmap like before, now contains more than one bitmap and things related to animation (see docs). A quick n dirty solution is to convert each of those drawables to a static bitmap using for example the technique in https://stackoverflow.com/questions/44447056/convert-adaptiveicondrawable-to-bitmap-in-android-o-preview . You will probably lose some fancies but it may be enough for your use case – rupps Oct 15 '19 at 03:24
1

You can use Glide for automatic loading and caching and the URI of each application icon:

final RequestManager mGlide = Glide.with(activity);

final Uri appIconUri = applicationInfo.icon != 0 ?
                Uri.parse("android.resource://" + packageName + "/" + applicationInfo.icon) :
                null;
if (appIconUri != null) mGlide.load(appIconUri).into(holder.appIconImgView);
else {
    mGlide.clear(holder.appIconImgView); // as suggested here: https://bumptech.github.io/glide/doc/getting-started.html
    mGlide.load(android.R.drawable.sym_def_app_icon).into(holder.appIconImgView);
}

The reason I suggest Glide and not other image loading libraries is that Glide supports XML drawable (or dynamic/adaptive or vector icons) loading while others don't (see https://github.com/facebook/fresco/issues/2173)

BamsBamx
  • 4,139
  • 4
  • 38
  • 63