• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.permissioncontroller.permission.ui.handheld;
18 
19 import static android.Manifest.permission_group.CAMERA;
20 import static android.Manifest.permission_group.LOCATION;
21 import static android.Manifest.permission_group.MICROPHONE;
22 
23 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED;
24 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS;
25 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM;
26 import static com.android.permissioncontroller.permission.debug.UtilsKt.shouldShowPermissionsDashboard;
27 
28 import android.app.AlertDialog;
29 import android.content.BroadcastReceiver;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentFilter;
33 import android.location.LocationManager;
34 import android.media.AudioManager;
35 import android.os.Bundle;
36 import android.os.UserHandle;
37 import android.text.Html;
38 import android.util.ArrayMap;
39 import android.util.ArraySet;
40 import android.util.Pair;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.ImageView;
45 import android.widget.TextView;
46 
47 import androidx.annotation.NonNull;
48 import androidx.annotation.Nullable;
49 import androidx.lifecycle.Observer;
50 import androidx.preference.PreferenceFragmentCompat;
51 
52 import com.android.permissioncontroller.PermissionControllerStatsLog;
53 import com.android.permissioncontroller.R;
54 import com.android.permissioncontroller.permission.data.OpAccess;
55 import com.android.permissioncontroller.permission.data.OpUsageLiveData;
56 import com.android.permissioncontroller.permission.debug.PermissionUsages;
57 import com.android.permissioncontroller.permission.model.AppPermissionGroup;
58 import com.android.permissioncontroller.permission.model.AppPermissionUsage;
59 import com.android.permissioncontroller.permission.model.AppPermissionUsage.GroupUsage;
60 import com.android.permissioncontroller.permission.model.legacy.PermissionApps;
61 import com.android.permissioncontroller.permission.model.legacy.PermissionApps.PermissionApp;
62 import com.android.permissioncontroller.permission.utils.KotlinUtils;
63 import com.android.permissioncontroller.permission.utils.Utils;
64 
65 import java.text.Collator;
66 import java.time.Instant;
67 import java.util.ArrayList;
68 import java.util.List;
69 import java.util.Map;
70 
71 /**
72  * A dialog listing the currently uses of camera, microphone, and location.
73  */
74 public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat {
75 
76     // TODO: Replace with OPSTR... APIs
77     static final String PHONE_CALL = "android:phone_call_microphone";
78     static final String VIDEO_CALL = "android:phone_call_camera";
79 
80     private AudioManager mAudioManager;
81     private @NonNull PermissionUsages mPermissionUsages;
82     private boolean mPermissionUsagesLoaded;
83     private @Nullable AlertDialog mDialog;
84     private OpUsageLiveData mOpUsageLiveData;
85     private @Nullable Map<String, List<OpAccess>> mOpUsage;
86     private ArraySet<String> mSystemUsage = new ArraySet<>(0);
87     private long mStartTime;
88 
89     private BroadcastReceiver mReceiver = new BroadcastReceiver() {
90         @Override
91         public void onReceive(Context context, Intent intent) {
92             onPermissionUsagesLoaded();
93         }
94     };
95 
96     /**
97      * @return A new {@link ReviewOngoingUsageFragment}
98      */
newInstance(long numMillis)99     public static ReviewOngoingUsageFragment newInstance(long numMillis) {
100         ReviewOngoingUsageFragment fragment = new ReviewOngoingUsageFragment();
101         Bundle arguments = new Bundle();
102         arguments.putLong(Intent.EXTRA_DURATION_MILLIS, numMillis);
103         fragment.setArguments(arguments);
104         return fragment;
105     }
106 
107     @Override
onCreate(Bundle savedInstanceState)108     public void onCreate(Bundle savedInstanceState) {
109         super.onCreate(savedInstanceState);
110 
111         long numMillis = getArguments().getLong(Intent.EXTRA_DURATION_MILLIS);
112 
113         mAudioManager = getContext().getSystemService(AudioManager.class);
114         getContext().registerReceiver(mReceiver,
115                 new IntentFilter(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED));
116         mPermissionUsages = new PermissionUsages(getActivity());
117         mStartTime = Math.max(System.currentTimeMillis() - numMillis, Instant.EPOCH.toEpochMilli());
118         String[] permissions = new String[]{CAMERA, MICROPHONE};
119         if (shouldShowPermissionsDashboard()) {
120             permissions = new String[] {CAMERA, LOCATION, MICROPHONE};
121         }
122         ArrayList<String> appOps = new ArrayList<>(List.of(PHONE_CALL, VIDEO_CALL));
123         mOpUsageLiveData = OpUsageLiveData.Companion.get(appOps, numMillis);
124         mOpUsageLiveData.observeStale(this, new Observer<Map<String, List<OpAccess>>>() {
125             @Override
126             public void onChanged(Map<String, List<OpAccess>> opUsage) {
127                 if (mOpUsageLiveData.isStale()) {
128                     return;
129                 }
130                 mOpUsage = opUsage;
131                 mOpUsageLiveData.removeObserver(this);
132 
133                 if (mPermissionUsagesLoaded) {
134                     onPermissionUsagesLoaded();
135                 }
136             }
137         });
138         mPermissionUsages.load(null, permissions, mStartTime, Long.MAX_VALUE,
139                 PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(), false, false,
140                 this::onPermissionUsagesLoaded, false);
141     }
142 
onPermissionUsagesLoaded()143     private void onPermissionUsagesLoaded() {
144         mPermissionUsagesLoaded = true;
145         if (getActivity() == null || mOpUsage == null) {
146             return;
147         }
148 
149         List<AppPermissionUsage> appPermissionUsages = mPermissionUsages.getUsages();
150 
151         List<Pair<AppPermissionUsage, List<GroupUsage>>> usages = new ArrayList<>();
152         ArrayList<PermissionApp> permApps = new ArrayList<>();
153         int numApps = appPermissionUsages.size();
154         for (int appNum = 0; appNum < numApps; appNum++) {
155             AppPermissionUsage appUsage = appPermissionUsages.get(appNum);
156 
157             List<GroupUsage> usedGroups = new ArrayList<>();
158             List<GroupUsage> appGroups = appUsage.getGroupUsages();
159             int numGroups = appGroups.size();
160             for (int groupNum = 0; groupNum < numGroups; groupNum++) {
161                 GroupUsage groupUsage = appGroups.get(groupNum);
162                 String groupName = groupUsage.getGroup().getName();
163 
164                 if (!groupUsage.isRunning()) {
165                     if (groupUsage.getLastAccessDuration() == -1) {
166                         if (groupUsage.getLastAccessTime() < mStartTime) {
167                             continue;
168                         }
169                     } else {
170                         // TODO: Warning: Only works for groups with a single permission as it is
171                         // not guaranteed the last access time and duration refer to same permission
172                         // in AppPermissionUsage#lastAccessAggregate
173                         if (groupUsage.getLastAccessTime() + groupUsage.getLastAccessDuration()
174                                 < mStartTime) {
175                             continue;
176                         }
177                     }
178                 }
179 
180                 if (Utils.isGroupOrBgGroupUserSensitive(groupUsage.getGroup())) {
181                     usedGroups.add(appGroups.get(groupNum));
182                 } else if (getContext().getSystemService(LocationManager.class).isProviderPackage(
183                         appUsage.getPackageName())
184                         && (groupName.equals(CAMERA) || groupName.equals(MICROPHONE))) {
185                     mSystemUsage.add(groupName);
186                 }
187             }
188 
189             if (!usedGroups.isEmpty()) {
190                 usages.add(Pair.create(appUsage, usedGroups));
191                 permApps.add(appUsage.getApp());
192             }
193         }
194 
195         if (usages.isEmpty() && mOpUsage.isEmpty() && mSystemUsage.isEmpty()) {
196             getActivity().finish();
197             return;
198         }
199 
200         new PermissionApps.AppDataLoader(getActivity(), () -> showDialog(usages))
201                 .execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()]));
202     }
203 
showDialog(@onNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages)204     private void showDialog(@NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) {
205         if (mDialog == null || !mDialog.isShowing()) {
206             AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
207                     .setView(createDialogView(usages))
208                     .setPositiveButton(R.string.ongoing_usage_dialog_ok, (dialog, which) ->
209                             PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED,
210                                     PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS))
211                     .setOnDismissListener((dialog) -> getActivity().finish());
212             mDialog = builder.create();
213             mDialog.show();
214         } else {
215             mDialog.setView(createDialogView(usages));
216         }
217     }
218 
219     /**
220      * Get a list of permission labels.
221      *
222      * @param groups map<perm group name, perm group label>
223      *
224      * @return A localized string with the list of permissions
225      */
getListOfPermissionLabels(ArrayMap<String, CharSequence> groups)226     private CharSequence getListOfPermissionLabels(ArrayMap<String, CharSequence> groups) {
227         int numGroups = groups.size();
228 
229         if (numGroups == 1) {
230             return groups.valueAt(0);
231         } else if (numGroups == 2 && groups.containsKey(MICROPHONE) && groups.containsKey(CAMERA)) {
232             // Special case camera + mic permission to be localization friendly
233             return getContext().getString(R.string.permgroup_list_microphone_and_camera);
234         } else {
235             // TODO: Use internationalization safe concatenation
236 
237             ArrayList<CharSequence> sortedGroups = new ArrayList<>(groups.values());
238             Collator collator = Collator.getInstance(
239                     getResources().getConfiguration().getLocales().get(0));
240             sortedGroups.sort(collator);
241 
242             StringBuilder listBuilder = new StringBuilder();
243 
244             for (int i = 0; i < numGroups; i++) {
245                 listBuilder.append(sortedGroups.get(i));
246                 if (i < numGroups - 2) {
247                     listBuilder.append(getString(R.string.ongoing_usage_dialog_separator));
248                 } else if (i < numGroups - 1) {
249                     listBuilder.append(getString(R.string.ongoing_usage_dialog_last_separator));
250                 }
251             }
252 
253             return listBuilder;
254         }
255     }
256 
createDialogView( @onNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages)257     private @NonNull View createDialogView(
258             @NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) {
259         Context context = getActivity();
260         LayoutInflater inflater = LayoutInflater.from(context);
261         View contentView = inflater.inflate(R.layout.ongoing_usage_dialog_content, null);
262         ViewGroup appsList = contentView.requireViewById(R.id.items_container);
263 
264         // Compute all of the permission group labels that were used.
265         ArrayMap<String, CharSequence> usedGroups = new ArrayMap<>();
266         int numUsages = usages.size();
267         for (int usageNum = 0; usageNum < numUsages; usageNum++) {
268             List<GroupUsage> groups = usages.get(usageNum).second;
269             int numGroups = groups.size();
270             for (int groupNum = 0; groupNum < numGroups; groupNum++) {
271                 AppPermissionGroup group = groups.get(groupNum).getGroup();
272                 if (group.getName().equals(MICROPHONE) && mAudioManager.isMicrophoneMute()) {
273                     continue;
274                 }
275                 usedGroups.put(group.getName(), group.getLabel());
276             }
277         }
278 
279         TextView otherUseHeader = contentView.requireViewById(R.id.other_use_header);
280         TextView otherUseContent = contentView.requireViewById(R.id.other_use_content);
281         TextView systemUseContent = contentView.requireViewById(R.id.system_use_content);
282         View otherUseSpacer = contentView.requireViewById(R.id.other_use_inside_spacer);
283 
284         if (mOpUsage.isEmpty() && mSystemUsage.isEmpty()) {
285             otherUseHeader.setVisibility(View.GONE);
286             otherUseContent.setVisibility(View.GONE);
287         }
288 
289         if (numUsages == 0) {
290             otherUseHeader.setVisibility(View.GONE);
291             appsList.setVisibility(View.GONE);
292         }
293 
294         if (mOpUsage.isEmpty() || mSystemUsage.isEmpty()) {
295             otherUseSpacer.setVisibility(View.GONE);
296         }
297 
298         if (mOpUsage.isEmpty()) {
299             otherUseContent.setVisibility(View.GONE);
300         }
301 
302         if (mSystemUsage.isEmpty()) {
303             systemUseContent.setVisibility(View.GONE);
304         }
305 
306         if (!mOpUsage.isEmpty()) {
307             boolean hasVideo = mOpUsage.containsKey(VIDEO_CALL);
308             boolean hasPhone = mOpUsage.containsKey(PHONE_CALL)
309                     && !mAudioManager.isMicrophoneMute();
310             if (hasVideo && hasPhone) {
311                 otherUseContent.setText(
312                         Html.fromHtml(getString(R.string.phone_call_uses_microphone_and_camera),
313                                 0));
314             } else if (hasVideo && mAudioManager.isMicrophoneMute()) {
315                 otherUseContent.setText(
316                         Html.fromHtml(getString(R.string.phone_call_uses_camera), 0));
317             } else if (hasPhone) {
318                 otherUseContent.setText(
319                         Html.fromHtml(getString(R.string.phone_call_uses_microphone), 0));
320             }
321 
322             if (hasVideo) {
323                 usedGroups.put(CAMERA, KotlinUtils.INSTANCE.getPermGroupLabel(context, CAMERA));
324                 if (!mAudioManager.isMicrophoneMute()) {
325                     usedGroups.put(MICROPHONE,
326                             KotlinUtils.INSTANCE.getPermGroupLabel(context, MICROPHONE));
327                 }
328             }
329 
330             if (hasPhone) {
331                 usedGroups.put(MICROPHONE,
332                         KotlinUtils.INSTANCE.getPermGroupLabel(context, MICROPHONE));
333             }
334         }
335 
336         if (!mSystemUsage.isEmpty()) {
337             if (mSystemUsage.contains(MICROPHONE) && mSystemUsage.contains(CAMERA)
338                     && !mAudioManager.isMicrophoneMute()) {
339                 systemUseContent.setText(getString(R.string.system_uses_microphone_and_camera));
340             } else if (mSystemUsage.contains(CAMERA)) {
341                 systemUseContent.setText(getString(R.string.system_uses_camera));
342             } else if (mSystemUsage.contains(MICROPHONE) && !mAudioManager.isMicrophoneMute()) {
343                 systemUseContent.setText(getString(R.string.system_uses_microphone));
344             }
345 
346             for (String usage : mSystemUsage) {
347                 usedGroups.put(usage, KotlinUtils.INSTANCE.getPermGroupLabel(context, usage));
348             }
349         }
350 
351         // Add the layout for each app.
352         for (int usageNum = 0; usageNum < numUsages; usageNum++) {
353             Pair<AppPermissionUsage, List<GroupUsage>> usage = usages.get(usageNum);
354             PermissionApp app = usage.first.getApp();
355             List<GroupUsage> groups = usage.second;
356 
357             // Check if this uses only mic permission. If the mic is muted, do not show
358             if (groups.size() == 1) {
359                 if (groups.get(0).getGroup().getName().equals(MICROPHONE)
360                         && mAudioManager.isMicrophoneMute()) {
361                     continue;
362                 }
363             }
364 
365             View itemView = inflater.inflate(R.layout.ongoing_usage_dialog_item, appsList, false);
366 
367             ((TextView) itemView.requireViewById(R.id.app_name)).setText(app.getLabel());
368             ((ImageView) itemView.requireViewById(R.id.app_icon)).setImageDrawable(app.getIcon());
369 
370             ArrayMap<String, CharSequence> usedGroupsThisApp = new ArrayMap<>();
371 
372             ViewGroup iconFrame = itemView.requireViewById(R.id.icons);
373             int numGroups = usages.get(usageNum).second.size();
374             for (int groupNum = 0; groupNum < numGroups; groupNum++) {
375                 AppPermissionGroup group = groups.get(groupNum).getGroup();
376                 if (mAudioManager.isMicrophoneMute() && group.getName().equals(MICROPHONE)) {
377                     continue;
378                 }
379 
380                 ViewGroup groupView = (ViewGroup) inflater.inflate(R.layout.image_view, null);
381                 ((ImageView) groupView.requireViewById(R.id.icon)).setImageDrawable(
382                         Utils.applyTint(context, group.getIconResId(),
383                                 android.R.attr.colorControlNormal));
384                 iconFrame.addView(groupView);
385 
386                 usedGroupsThisApp.put(group.getName(), group.getLabel());
387             }
388             iconFrame.setVisibility(View.VISIBLE);
389 
390             TextView permissionsList = itemView.requireViewById(R.id.permissionsList);
391             permissionsList.setText(getListOfPermissionLabels(usedGroupsThisApp));
392             permissionsList.setVisibility(View.VISIBLE);
393 
394             itemView.setOnClickListener((v) -> {
395                 String packageName = app.getPackageName();
396                 PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED,
397                         PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM);
398                 UserHandle user = UserHandle.getUserHandleForUid(app.getUid());
399                 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
400                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
401                 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
402                 intent.putExtra(Intent.EXTRA_USER, user);
403                 context.startActivity(intent);
404                 mDialog.dismiss();
405             });
406 
407             appsList.addView(itemView);
408         }
409 
410         ((TextView) contentView.requireViewById(R.id.title)).setText(
411                 getString(R.string.ongoing_usage_dialog_title,
412                         getListOfPermissionLabels(usedGroups)));
413 
414         return contentView;
415     }
416 
417     @Override
onCreatePreferences(Bundle bundle, String s)418     public void onCreatePreferences(Bundle bundle, String s) {
419         // empty
420     }
421 
422     @Override
onDestroy()423     public void onDestroy() {
424         getContext().unregisterReceiver(mReceiver);
425         super.onDestroy();
426 
427     }
428 }
429