RecentLocationApps.java revision 8b4cdbebcfb53b57ac2afec1b1e3377d6a02bb21
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.settings.location;
18
19import android.app.AppOpsManager;
20import android.content.Context;
21import android.content.pm.ApplicationInfo;
22import android.content.pm.PackageManager;
23import android.graphics.drawable.Drawable;
24import android.os.Bundle;
25import android.preference.Preference;
26import android.preference.PreferenceActivity;
27import android.util.Log;
28
29import com.android.settings.R;
30import com.android.settings.applications.InstalledAppDetails;
31import com.android.settings.fuelgauge.BatterySipper;
32import com.android.settings.fuelgauge.BatteryStatsHelper;
33
34import java.util.ArrayList;
35import java.util.HashMap;
36import java.util.List;
37
38/**
39 * Retrieves the information of applications which accessed location recently.
40 */
41public class RecentLocationApps {
42    private static final String TAG = RecentLocationApps.class.getSimpleName();
43
44    private static final int RECENT_TIME_INTERVAL_MILLIS = 15 * 60 * 1000;
45
46    private final PreferenceActivity mActivity;
47    private final BatteryStatsHelper mStatsHelper;
48    private final PackageManager mPackageManager;
49
50    // Stores all the packages that requested location within the designated interval
51    // key - package name of the app
52    // value - whether the app has requested high power location
53
54    public RecentLocationApps(PreferenceActivity activity, BatteryStatsHelper sipperUtil) {
55        mActivity = activity;
56        mPackageManager = activity.getPackageManager();
57        mStatsHelper = sipperUtil;
58    }
59
60    private class UidEntryClickedListener
61            implements Preference.OnPreferenceClickListener {
62        private BatterySipper mSipper;
63
64        public UidEntryClickedListener(BatterySipper sipper) {
65            mSipper = sipper;
66        }
67
68        @Override
69        public boolean onPreferenceClick(Preference preference) {
70            mStatsHelper.startBatteryDetailPage(mActivity, mSipper, false);
71            return true;
72        }
73    }
74
75    private class PackageEntryClickedListener
76            implements Preference.OnPreferenceClickListener {
77        private String mPackage;
78
79        public PackageEntryClickedListener(String packageName) {
80            mPackage = packageName;
81        }
82
83        @Override
84        public boolean onPreferenceClick(Preference preference) {
85            // start new fragment to display extended information
86            Bundle args = new Bundle();
87            args.putString(InstalledAppDetails.ARG_PACKAGE_NAME, mPackage);
88            mActivity.startPreferencePanel(InstalledAppDetails.class.getName(), args,
89                    R.string.application_info_label, null, null, 0);
90            return true;
91        }
92    }
93
94    private Preference createRecentLocationEntry(
95            Drawable icon,
96            CharSequence label,
97            boolean isHighBattery,
98            Preference.OnPreferenceClickListener listener) {
99        Preference pref = new Preference(mActivity);
100        pref.setIcon(icon);
101        pref.setTitle(label);
102        if (isHighBattery) {
103            pref.setSummary(R.string.location_high_battery_use);
104        } else {
105            pref.setSummary(R.string.location_low_battery_use);
106        }
107        pref.setOnPreferenceClickListener(listener);
108        return pref;
109    }
110
111    /**
112     * Stores a BatterySipper object and records whether the sipper has been used.
113     */
114    private static final class BatterySipperWrapper {
115        private BatterySipper mSipper;
116        private boolean mUsed;
117
118        public BatterySipperWrapper(BatterySipper sipper) {
119            mSipper = sipper;
120            mUsed = false;
121        }
122
123        public BatterySipper batterySipper() {
124            return mSipper;
125        }
126
127        public boolean used() {
128            return mUsed;
129        }
130
131        public void setUsed() {
132            mUsed = true;
133        }
134    }
135
136    /**
137     * Fills a list of applications which queried location recently within
138     * specified time.
139     */
140    public List<Preference> getAppList() {
141        // Retrieve Uid-based battery blaming info and generate a package to BatterySipper HashMap
142        // for later faster looking up.
143        mStatsHelper.refreshStats(true);
144        List<BatterySipper> usageList = mStatsHelper.getUsageList();
145        // Key: package Uid. Value: BatterySipperWrapper.
146        HashMap<Integer, BatterySipperWrapper> sipperMap =
147                new HashMap<Integer, BatterySipperWrapper>(usageList.size());
148        for (BatterySipper sipper: usageList) {
149            int uid = sipper.getUid();
150            if (uid != 0) {
151                sipperMap.put(uid, new BatterySipperWrapper(sipper));
152            }
153        }
154
155        // Retrieve a location usage list from AppOps
156        AppOpsManager aoManager =
157                (AppOpsManager) mActivity.getSystemService(Context.APP_OPS_SERVICE);
158        List<AppOpsManager.PackageOps> appOps = aoManager.getPackagesForOps(
159                new int[] {
160                    AppOpsManager.OP_MONITOR_LOCATION,
161                    AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION,
162                });
163
164        // Process the AppOps list and generate a preference list.
165        ArrayList<Preference> prefs = new ArrayList<Preference>();
166        long now = System.currentTimeMillis();
167        for (AppOpsManager.PackageOps ops : appOps) {
168            BatterySipperWrapper wrapper = sipperMap.get(ops.getUid());
169            Preference pref = getPreferenceFromOps(now, ops, wrapper);
170            if (pref != null) {
171                prefs.add(pref);
172            }
173        }
174
175        return prefs;
176    }
177
178    /**
179     * Creates a Preference entry for the given PackageOps.
180     *
181     * This method examines the time interval of the PackageOps first. If the PackageOps is older
182     * than the designated interval, this method ignores the PackageOps object and returns null.
183     *
184     * When the PackageOps is fresh enough, if the package has a corresponding battery blaming entry
185     * in the Uid-based battery sipper list, this method returns a Preference pointing to the Uid
186     * battery blaming page. If the package doesn't have a battery sipper entry (typically shouldn't
187     * happen), this method returns a Preference pointing to the App Info page for that package.
188     */
189    private Preference getPreferenceFromOps(
190            long now,
191            AppOpsManager.PackageOps ops,
192            BatterySipperWrapper wrapper) {
193        String packageName = ops.getPackageName();
194        List<AppOpsManager.OpEntry> entries = ops.getOps();
195        boolean highBattery = false;
196        boolean normalBattery = false;
197        // Earliest time for a location request to end and still be shown in list.
198        long recentLocationCutoffTime = now - RECENT_TIME_INTERVAL_MILLIS;
199        for (AppOpsManager.OpEntry entry : entries) {
200            if (entry.isRunning() || entry.getTime() >= recentLocationCutoffTime) {
201                switch (entry.getOp()) {
202                    case AppOpsManager.OP_MONITOR_LOCATION:
203                        normalBattery = true;
204                        break;
205                    case AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION:
206                        highBattery = true;
207                        break;
208                    default:
209                        break;
210                }
211            }
212        }
213
214        if (!highBattery && !normalBattery) {
215            if (Log.isLoggable(TAG, Log.VERBOSE)) {
216                Log.v(TAG, packageName + " hadn't used location within the time interval.");
217            }
218            return null;
219        }
220
221        // The package is fresh enough, continue.
222
223        Preference pref = null;
224        if (wrapper != null) {
225            // Contains sipper. Link to Battery Blaming page.
226
227            // We're listing by UID rather than package. Check whether the entry has been used
228            // before to prevent the same UID from showing up twice.
229            if (!wrapper.used()) {
230                BatterySipper sipper = wrapper.batterySipper();
231                sipper.loadNameAndIcon();
232                pref = createRecentLocationEntry(
233                        sipper.getIcon(),
234                        sipper.getLabel(),
235                        highBattery,
236                        new UidEntryClickedListener(sipper));
237                wrapper.setUsed();
238            }
239        } else {
240            // No corresponding sipper. Link to App Info page.
241
242            // This is grouped by package rather than UID, but that's OK because this branch
243            // shouldn't happen in practice.
244            try {
245                ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
246                        packageName, PackageManager.GET_META_DATA);
247                // Multiple users can install the same package. Each user gets a different Uid for
248                // the same package.
249                //
250                // Here we retrieve the Uid with package name, that will be the Uid for that package
251                // associated with the current active user. If the Uid differs from the Uid in ops,
252                // that means this entry belongs to another inactive user and we should ignore that.
253                if (appInfo.uid == ops.getUid()) {
254                    pref = createRecentLocationEntry(
255                            mPackageManager.getApplicationIcon(appInfo),
256                            mPackageManager.getApplicationLabel(appInfo),
257                            highBattery,
258                            new PackageEntryClickedListener(packageName));
259                } else if (Log.isLoggable(TAG, Log.VERBOSE)) {
260                    Log.v(TAG, "package " + packageName + " with Uid " + ops.getUid() +
261                            " belongs to another inactive account, ignored.");
262                }
263            } catch (PackageManager.NameNotFoundException e) {
264                Log.wtf(TAG, "Package not found: " + packageName);
265            }
266        }
267
268        return pref;
269    }
270}
271