Sort recent apps, show message when no recent apps

Change-Id: I70bb1913435b46fc87aec10075e43f98ef07ef62
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6f242ca..b49d929 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -2377,6 +2377,8 @@
     <string name="location_mode_location_off_title">Location off</string>
     <!-- [CHAR LIMIT=30] Location settings screen, sub category for recent location requests -->
     <string name="location_category_recent_location_requests">Recent location requests</string>
+    <!-- [CHAR LIMIT=30] Location settings screen, displayed when there's no recent app accessing location -->
+    <string name="location_no_recent_apps">No recent apps</string>
     <!-- [CHAR LIMIT=30] Location settings screen, sub category for location services -->
     <string name="location_category_location_services">Location services</string>
     <!-- [CHAR LIMIT=30] Location settings screen, recent location requests high battery use-->
diff --git a/src/com/android/settings/fuelgauge/BatterySipper.java b/src/com/android/settings/fuelgauge/BatterySipper.java
index 5245434..fcc8f69 100644
--- a/src/com/android/settings/fuelgauge/BatterySipper.java
+++ b/src/com/android/settings/fuelgauge/BatterySipper.java
@@ -119,6 +119,14 @@
         return mPackages;
     }
 
+    public int getUid() {
+        // Bail out if the current sipper is not an App sipper.
+        if (uidObj == null) {
+            return 0;
+        }
+        return uidObj.getUid();
+    }
+
     void getQuickNameIconForUid(Uid uidObj) {
         final int uid = uidObj.getUid();
         final String uidString = Integer.toString(uid);
diff --git a/src/com/android/settings/location/RecentLocationApps.java b/src/com/android/settings/location/RecentLocationApps.java
index fda734c..9a59563 100644
--- a/src/com/android/settings/location/RecentLocationApps.java
+++ b/src/com/android/settings/location/RecentLocationApps.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.preference.Preference;
 import android.preference.PreferenceActivity;
@@ -33,9 +34,11 @@
 import com.android.settings.fuelgauge.BatterySipper;
 import com.android.settings.fuelgauge.BatteryStatsHelper;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Retrieves the information of applications which accessed location recently.
@@ -93,12 +96,69 @@
         }
     }
 
+    private PreferenceScreen createRecentLocationEntry(
+            PreferenceManager preferenceManager,
+            Drawable icon,
+            CharSequence label,
+            boolean isHighBattery,
+            Preference.OnPreferenceClickListener listener) {
+        PreferenceScreen screen = preferenceManager.createPreferenceScreen(mActivity);
+        screen.setIcon(icon);
+        screen.setTitle(label);
+        if (isHighBattery) {
+            screen.setSummary(R.string.location_high_battery_use);
+        } else {
+            screen.setSummary(R.string.location_low_battery_use);
+        }
+        screen.setOnPreferenceClickListener(listener);
+        return screen;
+    }
+
+    /**
+     * Stores a BatterySipper object and records whether the sipper has been used.
+     */
+    private static final class BatterySipperWrapper {
+        private BatterySipper mSipper;
+        private boolean mUsed;
+
+        public BatterySipperWrapper(BatterySipper sipper) {
+            mSipper = sipper;
+            mUsed = false;
+        }
+
+        public BatterySipper batterySipper() {
+            return mSipper;
+        }
+
+        public boolean used() {
+            return mUsed;
+        }
+
+        public void setUsed() {
+            mUsed = true;
+        }
+    }
+
     /**
      * Fills a list of applications which queried location recently within
      * specified time.
      */
     public void fillAppList(PreferenceCategory container) {
-        HashMap<String, Boolean> packageMap = new HashMap<String, Boolean>();
+        // Retrieve Uid-based battery blaming info and generate a package to BatterySipper HashMap
+        // for later faster looking up.
+        mStatsHelper.refreshStats();
+        List<BatterySipper> usageList = mStatsHelper.getUsageList();
+        // Key: package Uid. Value: BatterySipperWrapper.
+        HashMap<Integer, BatterySipperWrapper> sipperMap =
+                new HashMap<Integer, BatterySipperWrapper>(usageList.size());
+        for (BatterySipper sipper: usageList) {
+            int uid = sipper.getUid();
+            if (uid != 0) {
+                sipperMap.put(uid, new BatterySipperWrapper(sipper));
+            }
+        }
+
+        // Retrieve a location usage list from AppOps
         AppOpsManager aoManager =
                 (AppOpsManager) mActivity.getSystemService(Context.APP_OPS_SERVICE);
         List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps(
@@ -107,68 +167,56 @@
                     AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION,
                 });
         PreferenceManager preferenceManager = container.getPreferenceManager();
+
+        // Process the AppOps list and generate a preference list.
+        ArrayList<PreferenceScreen> prefs = new ArrayList<PreferenceScreen>();
         long now = System.currentTimeMillis();
         for (AppOpsManager.PackageOps ops : appOps) {
-            processPackageOps(now, container, preferenceManager, ops, packageMap);
-        }
-
-        mStatsHelper.refreshStats();
-        List<BatterySipper> usageList = mStatsHelper.getUsageList();
-        for (BatterySipper sipper : usageList) {
-            sipper.loadNameAndIcon();
-            String[] packages = sipper.getPackages();
-            if (packages == null) {
-                continue;
-            }
-            for (String curPackage : packages) {
-                if (packageMap.containsKey(curPackage)) {
-                    PreferenceScreen screen = preferenceManager.createPreferenceScreen(mActivity);
-                    screen.setIcon(sipper.getIcon());
-                    screen.setTitle(sipper.getLabel());
-                    if (packageMap.get(curPackage)) {
-                        screen.setSummary(R.string.location_high_battery_use);
-                    } else {
-                        screen.setSummary(R.string.location_low_battery_use);
-                    }
-                    container.addPreference(screen);
-                    screen.setOnPreferenceClickListener(new UidEntryClickedListener(sipper));
-                    packageMap.remove(curPackage);
-                    break;
-                }
+            BatterySipperWrapper wrapper = sipperMap.get(ops.getUid());
+            PreferenceScreen screen = getScreenFromOps(preferenceManager, now, ops, wrapper);
+            if (screen != null) {
+                prefs.add(screen);
             }
         }
 
-        // Typically there shouldn't be any entry left in the HashMap. But if there are any, add
-        // them to the list and link them to the app info page.
-        for (Map.Entry<String, Boolean> entry : packageMap.entrySet()) {
-            try {
-                PreferenceScreen screen = preferenceManager.createPreferenceScreen(mActivity);
-                ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
-                        entry.getKey(), PackageManager.GET_META_DATA);
-                screen.setIcon(mPackageManager.getApplicationIcon(appInfo));
-                screen.setTitle(mPackageManager.getApplicationLabel(appInfo));
-                // if used both high and low battery within the time interval, show as "high
-                // battery"
-                if (entry.getValue()) {
-                    screen.setSummary(R.string.location_high_battery_use);
-                } else {
-                    screen.setSummary(R.string.location_low_battery_use);
+        if (prefs.size() > 0) {
+            // If there's some items to display, sort the items and add them to the container.
+            Collections.sort(prefs, new Comparator<PreferenceScreen>() {
+                @Override
+                public int compare(PreferenceScreen lhs, PreferenceScreen rhs) {
+                    return lhs.getTitle().toString().compareTo(rhs.getTitle().toString());
                 }
-                screen.setOnPreferenceClickListener(
-                        new PackageEntryClickedListener(entry.getKey()));
-                container.addPreference(screen);
-            } catch (PackageManager.NameNotFoundException e) {
-                // ignore the current app and move on to the next.
+            });
+            for (PreferenceScreen entry : prefs) {
+                container.addPreference(entry);
             }
+        } else {
+            // If there's no item to display, add a "No recent apps" item.
+            PreferenceScreen screen = preferenceManager.createPreferenceScreen(mActivity);
+            screen.setTitle(R.string.location_no_recent_apps);
+            screen.setSelectable(false);
+            screen.setEnabled(false);
+            container.addPreference(screen);
         }
     }
 
-    private void processPackageOps(
-            long now,
-            PreferenceCategory container,
+    /**
+     * Creates a PreferenceScreen entry for the given PackageOps.
+     *
+     * This method examines the time interval of the PackageOps first. If the PackageOps is older
+     * than the designated interval, this method ignores the PackageOps object and returns null.
+     *
+     * When the PackageOps is fresh enough, if the package has a corresponding battery blaming entry
+     * in the Uid-based battery sipper list, this method returns a PreferenceScreen pointing to the
+     * Uid battery blaming page. If the package doesn't have a battery sipper entry (typically
+     * shouldn't happen), this method returns a PreferenceScreen pointing to the App Info page for
+     * that package.
+     */
+    private PreferenceScreen getScreenFromOps(
             PreferenceManager preferenceManager,
+            long now,
             AppOpsManager.PackageOps ops,
-            HashMap<String, Boolean> packageMap) {
+            BatterySipperWrapper wrapper) {
         String packageName = ops.getPackageName();
         List<AppOpsManager.OpEntry> entries = ops.getOps();
         boolean highBattery = false;
@@ -193,9 +241,47 @@
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
                 Log.v(TAG, packageName + " hadn't used location within the time interval.");
             }
-            return;
+            return null;
         }
 
-        packageMap.put(packageName, highBattery);
+        // The package is fresh enough, continue.
+
+        PreferenceScreen screen = null;
+        if (wrapper != null) {
+            // Contains sipper. Link to Battery Blaming page.
+
+            // We're listing by UID rather than package. Check whether the entry has been used
+            // before to prevent the same UID from showing up twice.
+            if (!wrapper.used()) {
+                BatterySipper sipper = wrapper.batterySipper();
+                sipper.loadNameAndIcon();
+                screen = createRecentLocationEntry(
+                        preferenceManager,
+                        sipper.getIcon(),
+                        sipper.getLabel(),
+                        highBattery,
+                        new UidEntryClickedListener(sipper));
+                wrapper.setUsed();
+            }
+        } else {
+            // No corresponding sipper. Link to App Info page.
+
+            // This is grouped by package rather than UID, but that's OK because this branch
+            // shouldn't happen in practice.
+            try {
+                ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
+                        packageName, PackageManager.GET_META_DATA);
+                screen = createRecentLocationEntry(
+                        preferenceManager,
+                        mPackageManager.getApplicationIcon(appInfo),
+                        mPackageManager.getApplicationLabel(appInfo),
+                        highBattery,
+                        new PackageEntryClickedListener(packageName));
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.wtf(TAG, "Package not found: " + packageName);
+            }
+        }
+
+        return screen;
     }
 }