org.jsharkey.grouphome.LauncherActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.jsharkey.grouphome.LauncherActivity.java

Source

/*
   Copyright (C) 2008 Jeffrey Sharkey, http://jsharkey.org/
       
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.
       
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
       
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.jsharkey.grouphome;

import java.text.Collator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONObject;

import android.app.Activity;
import android.app.ExpandableListActivity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.TextView;
import android.widget.Toast;

public class LauncherActivity extends ExpandableListActivity
        implements OnCreateContextMenuListener, OnClickListener {

    public static final String TAG = LauncherActivity.class.toString();

    //public static final String JSON_GROUP = "{'Apps: Communication': ['com.android.contacts','com.android.mms','com.android.browser','com.android.im','com.android.email','com.google.android.gm'],'Apps: Multimedia': ['com.android.camera','com.android.music','com.amazon.mp3','com.google.android.youtube'],'Apps: Travel': ['com.google.android.apps.maps'],'Apps: Tools': ['com.android.alarmclock','com.android.calculator2','com.android.voicedialer','com.android.vending','com.android.settings'],'Apps: Productivity':['com.android.calendar','com.android.todo','com.android.notepad'],'Apps: Demo': ['com.example.android.apis','com.android.development']}";
    public static final String JSON_GROUP = "{'Apps: Lifestyle': ['us.cirion.wheeler', 'com.csi.android.MyMedBox', 'com.android.xmas_tree', 'com.android.pedometer', 'com.speedymarks.android.calories', 'net.lifeaware', 'com.android.drums', 'ulzii.android.todoremember', 'com.android.lt.caloriescalculator', 'info.androidz.horoscope', 'com.android.demo.notepad2', 'frogarmy.pray', 'com.ricket.doug.nightclock', 'com.android.FatCalculator10', 'com.roundhill.androidWP', 'org.icount.beer', 'com.android.bartender', 'com.teedroid.caddy.android', 'net.jeremye.android.mrliver', 'com.digitalbias.android', 'Unyverse.android', 'com.navee.android.freefamilywatch', 'anvillar.rt', 'com.skycoders.quote', 'android.GolfTracks', 'leanCalc.leanCalc', 'com.google.android.noisealert', 'com.boorah.android', 'com.fatsecret.android', 'com.gasolin.android.gbmi', 'com.gasolin.android.abmi', 'com.wsl.CardioTrainer', 'com.highwaynorth.jogtracker', 'jerboid.eco2go', 'com.smartcapsules.cooking.android.taster', 'com.myclo.android.mycloset'], 'Apps: News & Weather': ['com.atlantistech.android.tideapp', 'com.pureinnovation.purerss', 'com.grss.ui', 'net.yesiltas.ascore', 'thinlet.moon', 'com.aws.android', 'thinlet.forecast', 'com.accuweather.android', 'com.codeshogun.android.nprstationnearyou', 'com.androidforums.election', 'com.ebessette.android.greadernotifier', 'com.weathertopconsulting.handwx', 'com.android.weather', 'org.anddev.andweather.apk', 'com.android.tmg', 'com.weather.Weather'], 'Games: Cards & Casino': ['com.fawepark.android.barcodebeasties', 'com.kmagic.solitaire', 'com.tyler.th', 'jp.hudson.android.reversi', 'jp.hudson.android.klondike', 'jp.hudson.android.blackjack'], 'Games: Casual': ['com.androidcan.connect4', 'com.FindIt', 'com.es.android', 'com.magmasoftware', 'com.kb.andriod.bubblewrap', 'com.jamoes.txtspeed', 'com.diota.android.taptick', 'com.eanixlive.speed', 'de.joergjahnke.c64.android', 'com.odesys.bgmon', 'com.google.android.divideandconquer', 'de.joergjahnke.gameboy.android', 'com.silvermoon.client', 'com.maplekeycompany.games.poppoppopcorn', 'com.applimobile.android.rotomem', 'com.gameloft.bubblebash', 'com.zelfi.android.joyity', 'com.example.amazed', 'com.mattwach.trap2', 'com.alfray.asqare', 'com.glu.android.bonsai'], 'Games: Brain & Puzzle': ['com.vlm.client', 'com.androidcan.asudoku', 'com.g1port.PreTetris', 'tx.android.biolines', 'posimotion.Tic_Tac_Toe', 'com.google.android.checkers', 'com.demo.FourNumGuess', 'brad.android.TileGame', 'com.applimobile.powervocab', 'org.censorednet.android.MathPractice', 'org.hermit.netscramble', 'se.jompe.gaming', 'net.healeys.lexic', 'com.android.lt.tictactoe', 'com.google.code.twisty', 'com.google.android.reversi', 'com.google.android.chess', 'com.android.cocc', 'artfulbits.aiMinesweeper', 'com.bytescape.tetroid', 'com.hanoi', 'com.r2.MemCuad', 'com.android.miniMatcher', 'de.joergjahnke.mobilesudoku', 'com.mark.andguess', 'com.dynamix.mobile.SmartTacToe', 'com.icenta.sudoku.ui', 'com.sporksoft.slidepuzzle', 'com.google.android.sokoban', 'name.boyle.chris.sgtpuzzles', 'girlsskip.app', 'com.stelluxstudios.Android.Sudoku', 'com.nitoware.mahjongg', 'net.vgart.sokodroid', 'com.mgm.jumpy', 'mobi.dreamware.chessclock', 'com.google.marvin.androidsays', 'com.tapjoy.coloroid', 'org.anddev.andsudoku.apk', 'com.jamoes.lightsout', 'com.tapjoy.mismismatch', 'com.glu.android.bgd'], 'Apps: Finance': ['com.visa.android.GUI', 'com.speedymarks.android.rates', 'com.quirkconsulting.ticker', 'com.johnlauricella.mymoney1', 'jg.finance.tipjar', 'com.shareprice.app.android', 'com.android.gTip', 'net.gumbercules.loot', 'com.wsol.android.app', 'com.gbizapps.calc', 'com.threeaspen.merchant', 'com.highrf.stockapp', 'com.rawthought.herenow', 'com.tf', 'com.mayosmith.InflationMaster', 'org.fogproject.andriod.AndBankBook', 'com.twofuse.stocker', 'com.calculator.mortgage', 'com.blau.android.gmoney', 'app.money.firewallet', 'com.evancharlton.mileage', 'com.android.budget', 'net.thauvin.erik.android.tiproid', 'com.a0soft.gphone.aCurrency', 'jg.finance.moneydroid', 'com.quirkconsulting', 'org.anddev.andtip.apk', 'com.mpagano.geztip', 'com.techmethods.mileage', 'masterofmuppets.tipcalc', 'com.google.android.bistromath', 'com.savingadvice.tip_calculator', 'com.infonow.bofa'], 'Apps: Productivity': ['com.android.calendar','com.android.todo','com.android.notepad','com.connvision.mobileaccessor.android', 'com.android.ussdbal', 'thinlet.dof', 'net.ser1.timetracker', 'org.openintents.filemanager', 'e2m.android', 'org.mmin.handycalc', 'org.openintents.timesheet', 'com.android.lt.todolist', 'com.jdm.qSearch', 'com.android123.aRecorder', 'com.westtek.pdfviewer', 'com.android123.aBook', 'thinlet.worldclock', 'com.widgetop.android', 'thinlet.salat', 'org.openintents.countdown', 'com.google.android.wikinotes', 'com.omniture.android.dasboard.viewer', 'Unyverse.pro', 'com.akproduction.notepad', 'com.junobe.android.smsbook', 'com.maplekeycompany.apps.bgcal', 'com.fognl.android.ringcontrol', 'thinlet.twilight', 'net.thauvin.erik.android.googsms', 'com.splashid', 'com.twofuse.droidrecord', 'com.poidio.pTextEdit', 'com.nextmobileweb.dialzero', 'com.votereport.android', 'androidin.aReader', 'org.thismetalsky.calCOOLator', 'org.openintents.notepad', 'com.android.todo', 'org.openintents.news', 'com.iambic.android.tipper', 'com.iambic.android.googhelper', 'com.tom.medical.obdating', 'ch.tea.android.cardslight', 'com.funambol.android', 'com.google.android.quicklist'], 'Apps: Shopping': ['com.metaworldsolutions.android', 'com.bonfiremedia.android_ebay', 'com.glam.glammobile', 'com.ap.WootChecker', 'com.codeshogun.dealsdroid', 'com.google.code.p.localspinner', 'com.navee.android.isearch', 'com.biggu.shopsavvy', 'com.afarine.android.ashopper', 'be.jameswilliams.android.pleasebrowseme', 'org.openintents.shopping', 'com.yellowbook.android2', 'com.google.android.housing', 'com.google.zxing.client.android', 'com.compareeverywhere'], 'Apps: Communication': ['com.android.contacts','com.android.mms','com.android.browser','com.android.im','com.android.email','com.google.android.gm','com.starobject.android.starcontact', 'com.danapple.email', 'com.metakall.android.wallet', 'com.here_test.android', 'com.indiabolbol.hookup', 'com.cabildo.callingcard', 'com.blau.android.away', 'com.kolbysoft.steel', 'com.here.android', 'com.fsck.k9', 'com.hullomail.android.messaging', 'com.p1.chompsms', 'com.dattasmoon.aTweeter', 'com.dattasmoon.andFBChat', 'com.evancharlton.g1central', 'org.microemu.android.Browser', 'com.phonefusion.voicemailplus.android', 'net.locationmessenger.android', 'com.acode', 'com.phonalyzr', 'org.dubmenow.dub.activities', 'org.dotphone.android7.a7email', 'android.gpsmail', 'com.byarger.exchangeit', 'org.dotphone.android7', 'com.blau.android.quickcut', 'com.meebo', 'com.voxofon', 'com.blau.android.screenon', 'com.android', 'com.twitli.android.twitter', 'com.iskoot.android', 'android.mailchat', 'com.twidroid', 'com.multiplefacets.messenger', 'org.connectbot', 'de.shapeservices.implus'], 'Apps: Travel': ['com.google.android.apps.maps','org.kevinslist.android.caltrain', 'com.android.droidgap', 'com.fawepark.android.gridreference', 'org.andnav', 'com.speedymarks.android.translator', 'letufindme.com', 'au.com.jtribe.pinpoint', 'com.accutracking', 'com.clinkybot.parkmark', 'com.android.gastrip', 'com.xirgonium.android', 'com.wikitude', 'com.android.mission4u.iTubeRide', 'com.a0soft.gphone.aDialCode', 'au.com.jtribe.firepin', 'com.cab4me.android', 'com.deafcode.android.Orienteer', 'com.langtolang', 'com.lpontier.marvin', 'com.breadcrumbz'], 'Apps: Entertainment': ['com.plusmo.collegebasketballscores', 'com.bbm.android.cowbell', 'com.plusmo.soccerscores', 'eu.spvsoft.android.snowglobe', 'com.adrink', 'com.ambrosoft.nytflix', 'com.textonphone.activity', 'com.justnutz', 'com.android.basiccamera1', 'thefrenchpoet.android', 'com.android.wallswitch1', 'com.softwareforme.Life', 'com.speedymarks.android.hockey', 'org.pmix.ui', 'com.speedymarks.android.bball', 'com.plusmo.probasketballscores', 'com.speedymarks.android.football', 'com.zweder.sunlight', 'mobi.androidtunes', 'com.mobilefootie.fotmob', 'com.consumerdevices.magicball.android', 'com.android.leet.noise', 'com.stylem.wallpapers', 'com.bpb', 'com.plusmo.profootball', 'com.google.code.p.slideunlocker', 'mobi.androidwallpapers.wallpaperoid', 'com.google.code.p.mariosimulator', 'com.magic8ball', 'com.stylem.movies', 'com.netmite.andme', 'com.capaci.android.flashlight', 'com.android.krystleii', 'com.nextmobileweb.phoneflix', 'com.gamelion.globalfactbook', 'com.android.geobrowse', 'com.plusmo.collegefootball', 'com.stylem.greetings', 'com.truecountry', 'com.altitude', 'com.hho', 'com.esmusica', 'com.android.lolcat'], 'Apps: Software libraries': ['com.google.tts', 'org.openintents.convertcsv', 'org.openintents.voicenotes', 'jp.android_group.artoolkit', 'com.google.android.radar'], 'Apps: Reference': ['com.starobject.android.stardico', 'com.waterflea.chordchart', 'com.speedymarks.android.thesaurus', 'com.simonjudge.aspell', 'com.darkdesign.constitution', 'com.google.android.stardroid', 'com.simpletools.android.mc.main', 'com.cadreworks.bible', 'com.agilemedicine.BloodGas', 'com.simonjudge.wordnet', 'com.traxel.wethepeople', 'com.acrodesign.acrobiblelite', 'com.nextmobileweb.quickpedia', 'net.thauvin.erik.android.spellit', 'net.veveo.ui.android', 'com.agilemedicine.medsearch', 'com.Idealab.android.WikiDici', 'org.freedictionary', 'com.webascender.callerid', 'hongbo.wordmate', 'com.bonfiremedia.android_wikimobile'], 'Apps: Tools': ['com.android.alarmclock','com.android.calculator2','com.android.voicedialer','com.android.vending','com.android.settings','com.r2d2.QWifi', 'com.google.android.diskusage', 'com.alexis.portknocking', 'com.schwimmer.android.togglegps', 'com.codetastrophe.cellfinder', 'de.anno.android.missedCall', 'androidin.androidWhere', 'org.paulmach.textedit', 'com.samir.compactreader', 'com.schwimmer.android.togglebluetooth', 'my.flipclock', 'com.apksoftware.compass', 'com.schwimmer.android.togglewifi', 'com.noamwolf.android.androidfound', 'com.brilaps.android.txtract', 'com.speedymarks.android.speedometer', 'com.eyeonweb.acount', 'net.jaqpot.netcounter', 'com.iq_mobile.iqlight', 'droidsans.android.DroidSansTweakToolsLite', 'com.ap.DroidFtp', 'com.waterflea.wifiscan', 'com.android.MPGCalc', 'com.zicam', 'com.msync.gui', 'com.demo.YesNo', 'com.agilus.pachube', 'koushikdutta.screenshot', 'com.google.android.netmeter', 'com.tokasiki.android.phonerecorder', 'droidsans.android.DroidSansTweakTools', 'com.rerware.android.MyBackup', 'net.laserbunny.android.otp', 'com.android123.aSettings', 'koushikdutta.superuser', 'com.adamrocker.android.input.simeji', 'com.zmj.sms', 'com.example.android', 'com.clinkybot.geodroid', 'org.texteasy', 'com.codethought.android.androidconvert', 'com.timers', 'com.eyeonweb.pacer', 'com.noamwolf.android', 'org.greenmileage', 'com.handcent.sender', 'jp.chai.android.JSandBox', 'com.droidWake.app', 'com.mathinpublic.lengthConveter1001', 'com.archanet.serverup', 'com.android.apkinstaller', 'com.rcreations.dscalarmmonitor', 'com.acme.android.powermanager', 'com.zweder', 'com.ap.StopTimer', 'lysesoft.bucketupload', 'us.lindanrandy.cidrcalculator', 'org.openintents.updatechecker', 'com.android.crypt.barada', 'com.rerware.android.MyBookmarks', 'com.mshare.chinese', 'letufindme.OscanO.UI', 'com.google.android.polyglotz', 'com.sphericbox.syb', 'com.swwomm.ringtoggle', 'net.gasbot', 'com.lindaandny.lindamanager', 'org.dotphone.android.softkey', 'com.android.cm3', 'simplecode.fft.test.no.mic.one.dot.zero', 'com.johnlauricella.mytrends', 'com.android.flashlight', 'koushikdutta.klaxon', 'com.factory_h.owner', 'com.boundaryremainder.android', 'com.tokasiki.android.voicerecorder', 'com.bmsstopwatch', 'koushikdutta.telnet', 'android.closedid', 'org.efalk.rpncalc', 'net.everythingandroid.timer', 'com.skwid.systemmonitor', 'com.speedymarks.android.start', 'com.android.term', 'com.android.stopwatch', 'com.android.bacc', 'com.rcreations.ipcamviewer', 'com.metago.bender', 'bz.ktk.bubble', 'com.xtremelabs.android.speedtest', 'edu.mit.locale', 'com.mmg.appin', 'com.wrike.contactssync', 'com.androidnerds.utilities.Glance', 'com.bitsetters.android.passwordsafe', 'com.traxel.calcstra', 'com.cooolmagic.android.toggle', 'com.example.android.notepad', 'com.beust.android.translate', 'org.moellers.lucas', 'org.openintents.flashlight', 'org.ravelin.android', 'com.and.Calc', 'com.mshare.gui', 'com.android.utcclock', 'com.milk.realprog', 'com.teneke.flashlight', 'com.xcr.android.inetwork', 'com.poidio.ServiceViewer', 'com.tokasiki.android.dialcall', 'com.instamapper.gpstracker', 'org.dyndns.devesh.flashlight', 'com.antbs.android', 'com.mobeegal.android', 'com.android.taskswitcher', 'com.angryredplanet.android.rings_extended', 'org.curiouscreature.android.shutterspeed', 'com.tmobile.wifi', 'com.appdroid.anycut'], 'Apps: Multimedia': ['com.android.camera','com.android.music','com.amazon.mp3','com.google.android.youtube','com.mp1.livo', 'com.streamfurious.android.free', 'com.sass.andrum', 'net.jjc1138.android.scrobbler', 'com.tokasiki.android.shuffleplay', 'com.antbs.antplayer', 'com.android123.aPlayer', 'com.giantrabbit.nagare', 'com.rcptones.downloader', 'souvey.musical', 'lamp.lime.sand', 'org.iii.ro.iiivpa', 'com.multiplefacets.pictorial', 'com.snoggdoggler.android.doggcatcher', 'com.deafcode.android.Cinema', 'mypack.intents', 'larry.zou.colorfullife', 'hsware.HSTempo', 'com.ringermobile.app', 'vOICe.vOICe', 'com.pixelpipe.android', 'com.ajaxie.lastfm', 'com.fingerpainters.fingerpaint', 'com.mmg.playsvideo', 'com.ewebcomputing', 'org.tunescontrol', 'marcone.toddlerlock', 'org.gmote.client.android', 'com.ringdroid', 'com.appdroid.videoplayer', 'org.openintents.splashplay', 'com.google.android.panoramio', 'com.google.android.photostream', 'com.imeem.gynoid', 'com.shinycore.picsayfree', 'com.shazam.android', 'com.tunewiki.lyricplayer.android'], 'Apps: Demo': ['com.example.android.apis','com.android.development','com.qualcomm.qx.neocore', 'com.android.level', 'org.example.translate', 'org.jnikfarjam.gameoflife', 'com.concretesoftware.caloriecounter', 'com.alfray.mandelbrot'], 'Games: Arcade & Action': ['com.zingball', 'com.gamevil.bs08', 'com.gamevil.pow', 'pbf.android.animation.first', 'org.hermit.tiltlander', 'com.piggybank.tntmaniac', 'de.joergjahnke.mobileinvaders', 'android.com.abb', 'org.allbinary', 'org.two11me.apps.ZombiePoliticians.Debug', 'com.punchometer', 'org.allbinary.game.minispacewar.vector', 'net.peterd.zombierun', 'org.allbinary.game.santasworldwar', 'org.allbinary.game.zeptoracer', 'org.allbinary.game.minispacewar', 'net.rbgrn.lightracer', 'com.example.android.lunarlander', 'com.example.android.snake', 'com.google.clickin2dabeat', 'com.NamcoNetworks.PacMan'], 'Apps: Social': ['au.com.jtribe.shoutr', 'com.google.android.nextfolder', 'com.loopt', 'net.jjc1138.android.twitter', 'com.ubikod.android.ubikim.buddymob', 'com.abwaters.pingdroid', 'letufindme.com.UI', 'com.globalmotion.wherester', 'com.nextmobileweb.fbook', 'com.tomgibara.android.pintail', 'com.myspace.android', 'com.googlecode.statusinator', 'com.dytara.eljay', 'com.wertago', 'com.eventr', 'com.placelnk']}";

    class EntryInfo {
        ResolveInfo resolveInfo;
        CharSequence title;
        Drawable thumb;
    }

    // try resolving group for given package name
    // we should try caching this information for later speed-up
    protected String resolveGroup(JSONObject groupMap, String packageName) {
        try {
            for (Iterator keys = groupMap.keys(); keys.hasNext();) {
                String groupName = (String) keys.next();
                JSONArray packages = groupMap.getJSONArray(groupName);
                for (int i = 0; i < packages.length(); i++) {
                    if (packageName.equals(packages.getString(i)))
                        return groupName;
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Problem while trying to resolve group", e);
        }
        return GROUP_UNKNOWN;
    }

    private LayoutInflater inflater = null;
    private PackageManager pm = null;
    private AppDatabase appdb = null;

    // TODO: move uncat group name into strings.xml
    public String GROUP_UNKNOWN = "Uncategorized";
    private int iconSize = -1;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.act_launch);

        this.inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        // remember open/closed status when coming back later
        // try latching onto new package events

        pm = getPackageManager();
        appdb = new AppDatabase(LauncherActivity.this);

        // allow focus inside of rows to select children
        getExpandableListView().setItemsCanFocus(true);

        iconSize = (int) getResources().getDimension(android.R.dimen.app_icon_size);

    }

    public void onStart() {
        super.onStart();
        new ProcessTask().execute();
    }

    public void onStop() {
        super.onStop();
        this.setListAdapter(null);
    }

    public void onDestroy() {
        super.onDestroy();
        appdb.close();
    }

    private MenuItem force = null;

    private final static int STATE_UNKNOWN = 1, STATE_ALL_EXPAND = 2, STATE_ALL_COLLAP = 3;

    private int expandState = STATE_UNKNOWN;

    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

        Intent homeIntent = new Intent();
        homeIntent.setClassName("com.android.launcher", "com.android.launcher.Launcher");
        menu.add("Default home").setIcon(R.drawable.ic_menu_home).setIntent(homeIntent);

        menu.add("Search").setIcon(android.R.drawable.ic_menu_search);

        force = menu.add("Expand all").setIcon(android.R.drawable.ic_menu_share)
                .setOnMenuItemClickListener(new OnMenuItemClickListener() {
                    public boolean onMenuItemClick(MenuItem item) {
                        ExpandableListView listView = LauncherActivity.this.getExpandableListView();
                        ExpandableListAdapter adapter = LauncherActivity.this.getExpandableListAdapter();
                        switch (expandState) {
                        case STATE_UNKNOWN:
                        case STATE_ALL_COLLAP:
                            // when unknown or collapsed, we force all open
                            for (int i = 0; i < adapter.getGroupCount(); i++)
                                listView.expandGroup(i);
                            expandState = STATE_ALL_EXPAND;
                            break;
                        case STATE_ALL_EXPAND:
                            // when expanded, we force all closed
                            for (int i = 0; i < adapter.getGroupCount(); i++)
                                listView.collapseGroup(i);
                            expandState = STATE_ALL_COLLAP;
                            break;
                        }

                        return true;
                    }
                });

        menu.add("Refresh").setIcon(R.drawable.ic_menu_refresh)
                .setOnMenuItemClickListener(new OnMenuItemClickListener() {
                    public boolean onMenuItemClick(MenuItem item) {
                        // clear any database mappings and local json cache
                        // TODO: clear local json cache (when implemented)
                        appdb.deleteAllMappings();
                        setListAdapter(null);
                        new ProcessTask().execute();
                        return true;
                    }
                });

        Intent settingsIntent = new Intent(android.provider.Settings.ACTION_SETTINGS);
        settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
        menu.add("Settings").setIcon(android.R.drawable.ic_menu_preferences).setIntent(settingsIntent);

        return true;
    }

    public boolean onPrepareOptionsMenu(Menu menu) {
        switch (expandState) {
        case STATE_UNKNOWN:
        case STATE_ALL_COLLAP:
            force.setTitle("Expand all");
            break;
        case STATE_ALL_EXPAND:
            force.setTitle("Collapse all");
            break;
        }
        return true;
    }

    /**
     * Helper function to correctly add categorized apps to entryMap. Will
     * create internal ArrayList if doesn't exist for given category.
     */
    private void addMappingHelper(Map<String, List<EntryInfo>> entryMap, EntryInfo entry, String categoryName) {
        if (!entryMap.containsKey(categoryName))
            entryMap.put(categoryName, new ArrayList<EntryInfo>());
        entryMap.get(categoryName).add(entry);
    }

    /**
     * Task that reads all applications, sorting into categories as needed.
     */
    private class ProcessTask extends UserTask<Void, Void, GroupAdapter> {
        public GroupAdapter doInBackground(Void... params) {

            // final map used to store category mappings
            Map<String, List<EntryInfo>> entryMap = new HashMap<String, List<EntryInfo>>();

            // search for all launchable apps
            Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
            mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);

            List<ResolveInfo> apps = pm.queryIntentActivities(mainIntent, 0);
            List<EntryInfo> passone = new LinkedList<EntryInfo>(), passtwo = new LinkedList<EntryInfo>();

            for (ResolveInfo info : apps) {
                EntryInfo entry = new EntryInfo();

                // load details about this app
                entry.resolveInfo = info;
                entry.title = info.loadLabel(pm);
                if (entry.title == null)
                    entry.title = info.activityInfo.name;

                passone.add(entry);
            }

            Log.d(TAG, String.format("entering first pass with %d unresolved", passone.size()));

            for (EntryInfo entry : passone) {
                // try resolving category using internal database
                String packageName = entry.resolveInfo.activityInfo.packageName;
                try {
                    String categoryName = appdb.getCategory(packageName);
                    if (categoryName != null) {
                        // found category for this app, so record it
                        addMappingHelper(entryMap, entry, categoryName);

                        Log.d(TAG, String.format("found categoryName=%s for packageName=%s", categoryName,
                                packageName));

                    } else {
                        // otherwise keep around for later resolving
                        passtwo.add(entry);
                    }
                } catch (Exception e) {
                    Log.e(TAG, "Problem while trying to categorize app", e);
                }

            }

            Log.d(TAG, String.format("entering second pass with %d unresolved", passtwo.size()));

            // second pass tries resolving unknown apps
            if (passtwo.size() > 0) {
                JSONObject groupMap = new JSONObject();
                try {
                    groupMap = new JSONObject(JSON_GROUP);
                } catch (Exception e) {
                    Log.e(TAG, "Problem parsing category JSON", e);
                }

                for (EntryInfo entry : passtwo) {
                    String packageName = entry.resolveInfo.activityInfo.packageName;
                    String categoryName = resolveGroup(groupMap, packageName);
                    CharSequence descrip = entry.resolveInfo.activityInfo.applicationInfo.loadDescription(pm);
                    if (descrip == null)
                        descrip = "";

                    appdb.addMapping(packageName, categoryName, descrip.toString());
                    addMappingHelper(entryMap, entry, categoryName);

                    Log.d(TAG, String.format("found new mapping groupName=%s for packageName=%s", categoryName,
                            packageName));

                }
            }

            // sort each category of apps 
            final Collator collator = Collator.getInstance();
            for (String key : entryMap.keySet()) {
                Collections.sort(entryMap.get(key), new Comparator<EntryInfo>() {
                    public int compare(EntryInfo object1, EntryInfo object2) {
                        return collator.compare(object1.title, object2.title);
                    }
                });
            }

            // free any cache memory
            appdb.clearCache();

            // now that app tree is built, pass along to adapter
            return new GroupAdapter(entryMap);

        }

        @Override
        public void end(GroupAdapter result) {
            updateColumns(result, getResources().getConfiguration());
            setListAdapter(result);

            // request focus to activate dpad
            getExpandableListView().requestFocus();
        }

    }

    /**
     * Task for creating application thumbnails as needed.
     */
    private class ThumbTask extends UserTask<Object, Void, Object[]> {
        public Object[] doInBackground(Object... params) {
            EntryInfo info = (EntryInfo) params[0];

            // create actual thumbnail and pass along to gui thread
            Drawable icon = info.resolveInfo.loadIcon(pm);
            info.thumb = Utilities.createIconThumbnail(icon, iconSize);
            return params;
        }

        @Override
        public void end(Object... params) {
            EntryInfo info = (EntryInfo) params[0];
            TextView textView = (TextView) params[1];

            // dont bother updating if target has been recycled
            if (!info.equals(textView.getTag()))
                return;
            textView.setCompoundDrawablesWithIntrinsicBounds(null, info.thumb, null, null);
        }
    }

    /**
     * Force columns shown in adapter based on orientation.
     */
    private void updateColumns(GroupAdapter adapter, Configuration config) {
        adapter.setColumns((config.orientation == Configuration.ORIENTATION_PORTRAIT) ? 4 : 6);
    }

    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        this.updateColumns((GroupAdapter) this.getExpandableListAdapter(), newConfig);
    }

    /**
     * Special adapter to help provide application lists using expandable
     * categories. Specifically, it folds child items into grid-like columns
     * based on setColumns(), which is a hack. While it correctly recycles rows
     * when possible, this could be written much better.
     */
    public class GroupAdapter extends BaseExpandableListAdapter {

        private Map<String, List<EntryInfo>> entryMap;
        private String[] groupNames;

        private int columns = -1;

        public GroupAdapter(Map<String, List<EntryInfo>> entryMap) {
            this.entryMap = entryMap;
            this.groupNames = entryMap.keySet().toArray(new String[] {});
            Arrays.sort(this.groupNames);
        }

        /**
         * Force the number of columns to use when wrapping child elements.
         * Inflated children should have static widths.
         */
        public void setColumns(int columns) {
            this.columns = columns;
            this.notifyDataSetInvalidated();
        }

        public Object getGroup(int groupPosition) {
            return this.groupNames[groupPosition];
        }

        public int getGroupCount() {
            return this.groupNames.length;
        }

        public long getGroupId(int groupPosition) {
            return groupPosition;
        }

        public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
            if (convertView == null)
                convertView = inflater.inflate(R.layout.item_header, parent, false);

            String group = (String) this.getGroup(groupPosition);
            ((TextView) convertView.findViewById(android.R.id.text1)).setText(group);

            return convertView;
        }

        public Object getChild(int groupPosition, int childPosition) {
            // no good value when a row is actually multiple children
            return null;
        }

        public long getChildId(int groupPosition, int childPosition) {
            return childPosition;
        }

        public int getChildrenCount(int groupPosition) {
            // wrap children items into rows using column count
            int actualCount = entryMap.get(groupNames[groupPosition]).size();
            return (actualCount + (columns - 1)) / columns;
        }

        public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView,
                ViewGroup parent) {

            if (convertView == null)
                convertView = inflater.inflate(R.layout.item_row, parent, false);

            final ViewGroup viewGroup = (ViewGroup) convertView;

            // rebuild this row if columns changed
            if (viewGroup.getChildCount() != columns) {
                viewGroup.removeAllViews();
                for (int i = 0; i < columns; i++) {
                    View view = inflater.inflate(R.layout.item_entry, parent, false);
                    view.setOnClickListener(LauncherActivity.this);
                    view.setOnCreateContextMenuListener(LauncherActivity.this);
                    viewGroup.addView(view);
                }
            }

            List<EntryInfo> actualChildren = entryMap.get(groupNames[groupPosition]);
            int start = childPosition * columns, end = (childPosition + 1) * columns;

            for (int i = start; i < end; i++) {
                final TextView textView = (TextView) viewGroup.getChildAt(i - start);

                if (i < actualChildren.size()) {
                    // fill with actual child info if available
                    final EntryInfo info = actualChildren.get(i);
                    textView.setText(info.title);
                    textView.setTag(info);
                    textView.setVisibility(View.VISIBLE);

                    // generate thumbnail in usertask if not already cached
                    if (info.thumb != null) {
                        textView.setCompoundDrawablesWithIntrinsicBounds(null, info.thumb, null, null);
                    } else {
                        textView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
                        new ThumbTask().execute(info, textView);
                    }

                } else {
                    textView.setVisibility(View.INVISIBLE);
                }

            }

            return convertView;
        }

        public boolean isChildSelectable(int groupPosition, int childPosition) {
            return true;
        }

        public boolean hasStableIds() {
            return true;
        }

    }

    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        if (!(v.getTag() instanceof EntryInfo))
            return;
        EntryInfo info = (EntryInfo) v.getTag();

        final String packageName = info.resolveInfo.activityInfo.applicationInfo.packageName;

        menu.setHeaderTitle(info.title);

        Intent detailsIntent = new Intent();
        detailsIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
        detailsIntent.putExtra("com.android.settings.ApplicationPkgName", packageName);
        menu.add("App details").setIntent(detailsIntent);

        Intent deleteIntent = new Intent(Intent.ACTION_DELETE);
        deleteIntent.setData(Uri.parse("package:" + packageName));
        menu.add("Uninstall").setIntent(deleteIntent);

    }

    public void onClick(View v) {
        if (!(v.getTag() instanceof EntryInfo))
            return;
        EntryInfo info = (EntryInfo) v.getTag();

        // build actual intent for launching app
        Intent launch = new Intent(Intent.ACTION_MAIN);
        launch.addCategory(Intent.CATEGORY_LAUNCHER);
        launch.setComponent(new ComponentName(info.resolveInfo.activityInfo.applicationInfo.packageName,
                info.resolveInfo.activityInfo.name));
        launch.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);

        try {
            this.startActivity(launch);
        } catch (Exception e) {
            Toast.makeText(this, "Problem trying to launch application", Toast.LENGTH_SHORT).show();
            Log.e(TAG, "Problem trying to launch application", e);
        }

    }

}