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.CompoundButton;
33 import android.widget.ImageView;
34 import android.widget.ListView;
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 
248         List<AppOpEntry> mList;
249 
AppListAdapter(Context context)250         public AppListAdapter(Context context) {
251             mResources = context.getResources();
252             mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
253         }
254 
setData(List<AppOpEntry> data)255         public void setData(List<AppOpEntry> data) {
256             mList = data;
257             notifyDataSetChanged();
258         }
259 
260         @Override
getCount()261         public int getCount() {
262             return mList != null ? mList.size() : 0;
263         }
264 
265         @Override
getItem(int position)266         public AppOpEntry getItem(int position) {
267             return mList.get(position);
268         }
269 
270         @Override
getItemId(int position)271         public long getItemId(int position) {
272             return position;
273         }
274 
275         /**
276          * Populate new items in the list.
277          */
278         @Override
getView(int position, View convertView, ViewGroup parent)279         public View getView(int position, View convertView, ViewGroup parent) {
280             View view;
281 
282             if (convertView == null) {
283                 view = mInflater.inflate(R.layout.app_ops_item, parent, false);
284             } else {
285                 view = convertView;
286             }
287 
288             AppOpEntry item = getItem(position);
289             ((ImageView) view.findViewById(R.id.app_icon)).setImageDrawable(
290                     item.getAppEntry().getIcon());
291             ((TextView) view.findViewById(R.id.app_name)).setText(item.getAppEntry().getLabel());
292             ((TextView) view.findViewById(R.id.op_name)).setText(
293                     item.getTimeText(mResources, false));
294             view.findViewById(R.id.op_time).setVisibility(View.GONE);
295             ((CompoundButton) view.findViewById(R.id.op_switch)).setChecked(
296                     item.getPrimaryOpMode() == AppOpsManager.MODE_ALLOWED);
297 
298             return view;
299         }
300     }
301 
302     @Override
onCreate(Bundle savedInstanceState)303     public void onCreate(Bundle savedInstanceState) {
304         super.onCreate(savedInstanceState);
305         mState = new AppOpsState(getActivity());
306     }
307 
onActivityCreated(Bundle savedInstanceState)308     @Override public void onActivityCreated(Bundle savedInstanceState) {
309         super.onActivityCreated(savedInstanceState);
310 
311         // Give some text to display if there is no data.  In a real
312         // application this would come from a resource.
313         setEmptyText("No applications");
314 
315         // We have a menu item to show in action bar.
316         setHasOptionsMenu(true);
317 
318         // Create an empty adapter we will use to display the loaded data.
319         mAdapter = new AppListAdapter(getActivity());
320         setListAdapter(mAdapter);
321 
322         // Start out with a progress indicator.
323         setListShown(false);
324 
325         // Prepare the loader.
326         getLoaderManager().initLoader(0, null, this);
327     }
328 
onListItemClick(ListView l, View v, int position, long id)329     @Override public void onListItemClick(ListView l, View v, int position, long id) {
330         AppOpEntry entry = mAdapter.getItem(position);
331         if (entry != null) {
332             // We treat this as tapping on the check box, toggling the app op state.
333             CompoundButton sw = v.findViewById(R.id.op_switch);
334             boolean checked = !sw.isChecked();
335             sw.setChecked(checked);
336             AppOpsManager.OpEntry op = entry.getOpEntry(0);
337             int mode = checked ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
338             mState.getAppOpsManager().setMode(op.getOp(),
339                     entry.getAppEntry().getApplicationInfo().uid,
340                     entry.getAppEntry().getApplicationInfo().packageName,
341                     mode);
342             entry.overridePrimaryOpMode(mode);
343         }
344     }
345 
onCreateLoader(int id, Bundle args)346     @Override public Loader<List<AppOpEntry>> onCreateLoader(int id, Bundle args) {
347         Bundle fargs = getArguments();
348         AppOpsState.OpsTemplate template = null;
349         if (fargs != null) {
350             template = fargs.getParcelable("template");
351         }
352         return new AppListLoader(getActivity(), mState, template);
353     }
354 
onLoadFinished(Loader<List<AppOpEntry>> loader, List<AppOpEntry> data)355     @Override public void onLoadFinished(Loader<List<AppOpEntry>> loader, List<AppOpEntry> data) {
356         // Set the new data in the adapter.
357         mAdapter.setData(data);
358 
359         // The list should now be shown.
360         if (isResumed()) {
361             setListShown(true);
362         } else {
363             setListShownNoAnimation(true);
364         }
365     }
366 
onLoaderReset(Loader<List<AppOpEntry>> loader)367     @Override public void onLoaderReset(Loader<List<AppOpEntry>> loader) {
368         // Clear the data in the adapter.
369         mAdapter.setData(null);
370     }
371 }
372