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