1 /**
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations
14  * under the License.
15  */
16 
17 package com.android.settings.applications;
18 
19 import android.app.AppOpsManager;
20 import android.app.ListFragment;
21 import android.app.LoaderManager;
22 import android.content.AsyncTaskLoader;
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.Loader;
28 import android.content.pm.ActivityInfo;
29 import android.content.res.Configuration;
30 import android.content.res.Resources;
31 import android.os.Bundle;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.widget.BaseAdapter;
36 import android.widget.ImageView;
37 import android.widget.ListView;
38 import android.widget.Switch;
39 import android.widget.TextView;
40 
41 import com.android.settings.R;
42 import com.android.settings.SettingsActivity;
43 import com.android.settings.applications.AppOpsState.AppOpEntry;
44 
45 import java.util.List;
46 
47 public class AppOpsCategory extends ListFragment implements
48         LoaderManager.LoaderCallbacks<List<AppOpEntry>> {
49 
50     private static final int RESULT_APP_DETAILS = 1;
51 
52     AppOpsState mState;
53     boolean mUserControlled;
54 
55     // This is the Adapter being used to display the list's data.
56     AppListAdapter mAdapter;
57 
58     String mCurrentPkgName;
59 
AppOpsCategory()60     public AppOpsCategory() {
61     }
62 
AppOpsCategory(AppOpsState.OpsTemplate template)63     public AppOpsCategory(AppOpsState.OpsTemplate template) {
64         this(template, false);
65     }
66 
AppOpsCategory(AppOpsState.OpsTemplate template, boolean userControlled)67     public AppOpsCategory(AppOpsState.OpsTemplate template, boolean userControlled) {
68         Bundle args = new Bundle();
69         args.putParcelable("template", template);
70         args.putBoolean("userControlled", userControlled);
71         setArguments(args);
72     }
73 
74     /**
75      * Helper for determining if the configuration has changed in an interesting
76      * way so we need to rebuild the app list.
77      */
78     public static class InterestingConfigChanges {
79         final Configuration mLastConfiguration = new Configuration();
80         int mLastDensity;
81 
applyNewConfig(Resources res)82         boolean applyNewConfig(Resources res) {
83             int configChanges = mLastConfiguration.updateFrom(res.getConfiguration());
84             boolean densityChanged = mLastDensity != res.getDisplayMetrics().densityDpi;
85             if (densityChanged || (configChanges&(ActivityInfo.CONFIG_LOCALE
86                     |ActivityInfo.CONFIG_UI_MODE|ActivityInfo.CONFIG_SCREEN_LAYOUT)) != 0) {
87                 mLastDensity = res.getDisplayMetrics().densityDpi;
88                 return true;
89             }
90             return false;
91         }
92     }
93 
94     /**
95      * Helper class to look for interesting changes to the installed apps
96      * so that the loader can be updated.
97      */
98     public static class PackageIntentReceiver extends BroadcastReceiver {
99         final AppListLoader mLoader;
100 
PackageIntentReceiver(AppListLoader loader)101         public PackageIntentReceiver(AppListLoader loader) {
102             mLoader = loader;
103             IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
104             filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
105             filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
106             filter.addDataScheme("package");
107             mLoader.getContext().registerReceiver(this, filter);
108             // Register for events related to sdcard installation.
109             IntentFilter sdFilter = new IntentFilter();
110             sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
111             sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
112             mLoader.getContext().registerReceiver(this, sdFilter);
113         }
114 
onReceive(Context context, Intent intent)115         @Override public void onReceive(Context context, Intent intent) {
116             // Tell the loader about the change.
117             mLoader.onContentChanged();
118         }
119     }
120 
121     /**
122      * A custom Loader that loads all of the installed applications.
123      */
124     public static class AppListLoader extends AsyncTaskLoader<List<AppOpEntry>> {
125         final InterestingConfigChanges mLastConfig = new InterestingConfigChanges();
126         final AppOpsState mState;
127         final AppOpsState.OpsTemplate mTemplate;
128         final boolean mUserControlled;
129 
130         List<AppOpEntry> mApps;
131         PackageIntentReceiver mPackageObserver;
132 
AppListLoader(Context context, AppOpsState state, AppOpsState.OpsTemplate template, boolean userControlled)133         public AppListLoader(Context context, AppOpsState state, AppOpsState.OpsTemplate template,
134                 boolean userControlled) {
135             super(context);
136             mState = state;
137             mTemplate = template;
138             mUserControlled = userControlled;
139         }
140 
loadInBackground()141         @Override public List<AppOpEntry> loadInBackground() {
142             return mState.buildState(mTemplate, 0, null,
143                     mUserControlled ? AppOpsState.LABEL_COMPARATOR : AppOpsState.RECENCY_COMPARATOR);
144         }
145 
146         /**
147          * Called when there is new data to deliver to the client.  The
148          * super class will take care of delivering it; the implementation
149          * here just adds a little more logic.
150          */
deliverResult(List<AppOpEntry> apps)151         @Override public void deliverResult(List<AppOpEntry> apps) {
152             if (isReset()) {
153                 // An async query came in while the loader is stopped.  We
154                 // don't need the result.
155                 if (apps != null) {
156                     onReleaseResources(apps);
157                 }
158             }
159             List<AppOpEntry> oldApps = apps;
160             mApps = apps;
161 
162             if (isStarted()) {
163                 // If the Loader is currently started, we can immediately
164                 // deliver its results.
165                 super.deliverResult(apps);
166             }
167 
168             // At this point we can release the resources associated with
169             // 'oldApps' if needed; now that the new result is delivered we
170             // know that it is no longer in use.
171             if (oldApps != null) {
172                 onReleaseResources(oldApps);
173             }
174         }
175 
176         /**
177          * Handles a request to start the Loader.
178          */
onStartLoading()179         @Override protected void onStartLoading() {
180             // We don't monitor changed when loading is stopped, so need
181             // to always reload at this point.
182             onContentChanged();
183 
184             if (mApps != null) {
185                 // If we currently have a result available, deliver it
186                 // immediately.
187                 deliverResult(mApps);
188             }
189 
190             // Start watching for changes in the app data.
191             if (mPackageObserver == null) {
192                 mPackageObserver = new PackageIntentReceiver(this);
193             }
194 
195             // Has something interesting in the configuration changed since we
196             // last built the app list?
197             boolean configChange = mLastConfig.applyNewConfig(getContext().getResources());
198 
199             if (takeContentChanged() || mApps == null || configChange) {
200                 // If the data has changed since the last time it was loaded
201                 // or is not currently available, start a load.
202                 forceLoad();
203             }
204         }
205 
206         /**
207          * Handles a request to stop the Loader.
208          */
onStopLoading()209         @Override protected void onStopLoading() {
210             // Attempt to cancel the current load task if possible.
211             cancelLoad();
212         }
213 
214         /**
215          * Handles a request to cancel a load.
216          */
onCanceled(List<AppOpEntry> apps)217         @Override public void onCanceled(List<AppOpEntry> apps) {
218             super.onCanceled(apps);
219 
220             // At this point we can release the resources associated with 'apps'
221             // if needed.
222             onReleaseResources(apps);
223         }
224 
225         /**
226          * Handles a request to completely reset the Loader.
227          */
onReset()228         @Override protected void onReset() {
229             super.onReset();
230 
231             // Ensure the loader is stopped
232             onStopLoading();
233 
234             // At this point we can release the resources associated with 'apps'
235             // if needed.
236             if (mApps != null) {
237                 onReleaseResources(mApps);
238                 mApps = null;
239             }
240 
241             // Stop monitoring for changes.
242             if (mPackageObserver != null) {
243                 getContext().unregisterReceiver(mPackageObserver);
244                 mPackageObserver = null;
245             }
246         }
247 
248         /**
249          * Helper function to take care of releasing resources associated
250          * with an actively loaded data set.
251          */
onReleaseResources(List<AppOpEntry> apps)252         protected void onReleaseResources(List<AppOpEntry> apps) {
253             // For a simple List<> there is nothing to do.  For something
254             // like a Cursor, we would close it here.
255         }
256     }
257 
258     public static class AppListAdapter extends BaseAdapter {
259         private final Resources mResources;
260         private final LayoutInflater mInflater;
261         private final AppOpsState mState;
262         private final boolean mUserControlled;
263 
264         List<AppOpEntry> mList;
265 
AppListAdapter(Context context, AppOpsState state, boolean userControlled)266         public AppListAdapter(Context context, AppOpsState state, boolean userControlled) {
267             mResources = context.getResources();
268             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
269             mState = state;
270             mUserControlled = userControlled;
271         }
272 
setData(List<AppOpEntry> data)273         public void setData(List<AppOpEntry> data) {
274             mList = data;
275             notifyDataSetChanged();
276         }
277 
278         @Override
getCount()279         public int getCount() {
280             return mList != null ? mList.size() : 0;
281         }
282 
283         @Override
getItem(int position)284         public AppOpEntry getItem(int position) {
285             return mList.get(position);
286         }
287 
288         @Override
getItemId(int position)289         public long getItemId(int position) {
290             return position;
291         }
292 
293         /**
294          * Populate new items in the list.
295          */
getView(int position, View convertView, ViewGroup parent)296         @Override public View getView(int position, View convertView, ViewGroup parent) {
297             View view;
298 
299             if (convertView == null) {
300                 view = mInflater.inflate(R.layout.app_ops_item, parent, false);
301             } else {
302                 view = convertView;
303             }
304 
305             AppOpEntry item = getItem(position);
306             ((ImageView)view.findViewById(R.id.app_icon)).setImageDrawable(
307                     item.getAppEntry().getIcon());
308             ((TextView)view.findViewById(R.id.app_name)).setText(item.getAppEntry().getLabel());
309             if (mUserControlled) {
310                 ((TextView) view.findViewById(R.id.op_name)).setText(
311                         item.getTimeText(mResources, false));
312                 view.findViewById(R.id.op_time).setVisibility(View.GONE);
313                 ((Switch) view.findViewById(R.id.op_switch)).setChecked(
314                         item.getPrimaryOpMode() == AppOpsManager.MODE_ALLOWED);
315             } else {
316                 ((TextView) view.findViewById(R.id.op_name)).setText(item.getSummaryText(mState));
317                 ((TextView) view.findViewById(R.id.op_time)).setText(
318                         item.getTimeText(mResources, false));
319                 view.findViewById(R.id.op_switch).setVisibility(View.GONE);
320             }
321 
322             return view;
323         }
324     }
325 
326     @Override
onCreate(Bundle savedInstanceState)327     public void onCreate(Bundle savedInstanceState) {
328         super.onCreate(savedInstanceState);
329         mState = new AppOpsState(getActivity());
330         mUserControlled = getArguments().getBoolean("userControlled");
331     }
332 
onActivityCreated(Bundle savedInstanceState)333     @Override public void onActivityCreated(Bundle savedInstanceState) {
334         super.onActivityCreated(savedInstanceState);
335 
336         // Give some text to display if there is no data.  In a real
337         // application this would come from a resource.
338         setEmptyText("No applications");
339 
340         // We have a menu item to show in action bar.
341         setHasOptionsMenu(true);
342 
343         // Create an empty adapter we will use to display the loaded data.
344         mAdapter = new AppListAdapter(getActivity(), mState, mUserControlled);
345         setListAdapter(mAdapter);
346 
347         // Start out with a progress indicator.
348         setListShown(false);
349 
350         // Prepare the loader.
351         getLoaderManager().initLoader(0, null, this);
352     }
353 
354     // utility method used to start sub activity
startApplicationDetailsActivity()355     private void startApplicationDetailsActivity() {
356         // start new fragment to display extended information
357         Bundle args = new Bundle();
358         args.putString(AppOpsDetails.ARG_PACKAGE_NAME, mCurrentPkgName);
359 
360         SettingsActivity sa = (SettingsActivity) getActivity();
361         sa.startPreferencePanel(AppOpsDetails.class.getName(), args,
362                 R.string.app_ops_settings, null, this, RESULT_APP_DETAILS);
363     }
364 
onListItemClick(ListView l, View v, int position, long id)365     @Override public void onListItemClick(ListView l, View v, int position, long id) {
366         AppOpEntry entry = mAdapter.getItem(position);
367         if (entry != null) {
368             if (mUserControlled) {
369                 // We treat this as tapping on the check box, toggling the app op state.
370                 Switch sw = ((Switch) v.findViewById(R.id.op_switch));
371                 boolean checked = !sw.isChecked();
372                 sw.setChecked(checked);
373                 AppOpsManager.OpEntry op = entry.getOpEntry(0);
374                 int mode = checked ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
375                 mState.getAppOpsManager().setMode(op.getOp(),
376                         entry.getAppEntry().getApplicationInfo().uid,
377                         entry.getAppEntry().getApplicationInfo().packageName,
378                         mode);
379                 entry.overridePrimaryOpMode(mode);
380             } else {
381                 mCurrentPkgName = entry.getAppEntry().getApplicationInfo().packageName;
382                 startApplicationDetailsActivity();
383             }
384         }
385     }
386 
onCreateLoader(int id, Bundle args)387     @Override public Loader<List<AppOpEntry>> onCreateLoader(int id, Bundle args) {
388         Bundle fargs = getArguments();
389         AppOpsState.OpsTemplate template = null;
390         if (fargs != null) {
391             template = (AppOpsState.OpsTemplate)fargs.getParcelable("template");
392         }
393         return new AppListLoader(getActivity(), mState, template, mUserControlled);
394     }
395 
onLoadFinished(Loader<List<AppOpEntry>> loader, List<AppOpEntry> data)396     @Override public void onLoadFinished(Loader<List<AppOpEntry>> loader, List<AppOpEntry> data) {
397         // Set the new data in the adapter.
398         mAdapter.setData(data);
399 
400         // The list should now be shown.
401         if (isResumed()) {
402             setListShown(true);
403         } else {
404             setListShownNoAnimation(true);
405         }
406     }
407 
onLoaderReset(Loader<List<AppOpEntry>> loader)408     @Override public void onLoaderReset(Loader<List<AppOpEntry>> loader) {
409         // Clear the data in the adapter.
410         mAdapter.setData(null);
411     }
412 }
413