1 /*
2  * Copyright (C) 2022 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.v31;
18 
19 import static android.Manifest.permission_group.CAMERA;
20 import static android.Manifest.permission_group.MICROPHONE;
21 
22 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED;
23 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS;
24 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM;
25 
26 import android.app.Activity;
27 import android.app.AlertDialog;
28 import android.content.Intent;
29 import android.icu.text.ListFormatter;
30 import android.os.Bundle;
31 import android.os.UserHandle;
32 import android.text.Html;
33 import android.util.ArrayMap;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.ImageView;
38 import android.widget.TextView;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 import androidx.lifecycle.ViewModelProvider;
43 import androidx.preference.PreferenceFragmentCompat;
44 
45 import com.android.permissioncontroller.PermissionControllerStatsLog;
46 import com.android.permissioncontroller.R;
47 import com.android.permissioncontroller.permission.ui.model.v31.ReviewOngoingUsageViewModel;
48 import com.android.permissioncontroller.permission.ui.model.v31.ReviewOngoingUsageViewModel.PackageAttribution;
49 import com.android.permissioncontroller.permission.ui.model.v31.ReviewOngoingUsageViewModelFactory;
50 import com.android.permissioncontroller.permission.utils.KotlinUtils;
51 import com.android.permissioncontroller.permission.utils.Utils;
52 
53 import java.text.Collator;
54 import java.util.ArrayList;
55 import java.util.Collection;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Set;
59 
60 /**
61  * A dialog listing the currently uses of camera, microphone, and location.
62  */
63 public class ReviewOngoingUsageFragment extends PreferenceFragmentCompat {
64     private static final String LOG_TAG = ReviewOngoingUsageFragment.class.getSimpleName();
65 
66     // TODO: Replace with OPSTR... APIs
67     public static final String PHONE_CALL = "android:phone_call_microphone";
68     public static final String VIDEO_CALL = "android:phone_call_camera";
69 
70     private @Nullable AlertDialog mDialog;
71 
72     private ReviewOngoingUsageViewModel mViewModel;
73 
74     // create new ViewModel in onStart, because viewModel is sometimes persisting after finish()
75     // TODO: determine why viewModel is doing this.
76     @Override
onStart()77     public void onStart() {
78         super.onStart();
79 
80         ReviewOngoingUsageViewModelFactory factory =
81                 new ReviewOngoingUsageViewModelFactory(
82                         getArguments().getLong(Intent.EXTRA_DURATION_MILLIS), this, new Bundle());
83         mViewModel = new ViewModelProvider(this, factory).get(ReviewOngoingUsageViewModel.class);
84 
85         mViewModel.getUsages().observe(this, usages -> {
86             if (mViewModel.getUsages().isStale()) {
87                 // Prevent stale data from being shown, if Dialog is shown twice in quick succession
88                 return;
89             }
90             if (usages == null) {
91                 getActivity().finishAfterTransition();
92                 return;
93             }
94 
95             if (mDialog == null) {
96                 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
97                         .setView(updateDialogView(usages))
98                         .setPositiveButton(R.string.ongoing_usage_dialog_ok, (dialog, which) ->
99                                 PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED,
100                                         PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_DISMISS))
101                         .setOnDismissListener((dialog) -> getActivity().finishAfterTransition());
102                 mDialog = builder.create();
103                 mDialog.show();
104             } else {
105                 updateDialogView(usages);
106             }
107             mViewModel.getUsages().removeObservers(this);
108         });
109     }
110 
111     @Override
onPause()112     public void onPause() {
113         super.onPause();
114         if (mDialog != null && getActivity() != null && !getActivity().isFinishing()) {
115             mDialog.dismiss();
116         }
117     }
118 
119     /**
120      * Get a list of permission labels.
121      *
122      * @param groups map<perm group name, perm group label>
123      *
124      * @return A localized string with the list of permissions
125      */
getListOfPermissionLabels(ArrayMap<String, CharSequence> groups)126     private CharSequence getListOfPermissionLabels(ArrayMap<String, CharSequence> groups) {
127         int numGroups = groups.size();
128 
129         if (numGroups == 1) {
130             return groups.valueAt(0);
131         } else if (numGroups == 2 && groups.containsKey(MICROPHONE) && groups.containsKey(CAMERA)) {
132             // Special case camera + mic permission to be localization friendly
133             return getContext().getString(R.string.permgroup_list_microphone_and_camera);
134         } else {
135             ArrayList<CharSequence> sortedGroups = new ArrayList<>(groups.values());
136             Collator collator = Collator.getInstance(
137                     getResources().getConfiguration().getLocales().get(0));
138             sortedGroups.sort(collator);
139 
140             StringBuilder listBuilder = new StringBuilder();
141 
142             for (int i = 0; i < numGroups; i++) {
143                 listBuilder.append(sortedGroups.get(i));
144                 if (i < numGroups - 2) {
145                     listBuilder.append(getString(R.string.ongoing_usage_dialog_separator));
146                 } else if (i < numGroups - 1) {
147                     listBuilder.append(getString(R.string.ongoing_usage_dialog_last_separator));
148                 }
149             }
150 
151             return listBuilder;
152         }
153     }
154 
updateDialogView(@onNull ReviewOngoingUsageViewModel.Usages allUsages)155     private @NonNull View updateDialogView(@NonNull ReviewOngoingUsageViewModel.Usages allUsages) {
156         Activity context = getActivity();
157 
158         LayoutInflater inflater = LayoutInflater.from(context);
159         View contentView = inflater.inflate(R.layout.ongoing_usage_dialog_content, null);
160         ViewGroup appsList = contentView.requireViewById(R.id.items_container);
161         Map<PackageAttribution, Set<String>> appUsages = allUsages.getAppUsages();
162         Collection<String> callUsage = allUsages.getCallUsages();
163         Map<PackageAttribution, List<CharSequence>> attrLabels = allUsages.getShownAttributions();
164 
165         // Compute all of the permission group labels that were used.
166         ArrayMap<String, CharSequence> usedGroups = new ArrayMap<>();
167         for (Set<String> accessedPermGroupNames : appUsages.values()) {
168             for (String accessedPermGroupName : accessedPermGroupNames) {
169                 usedGroups.put(accessedPermGroupName, KotlinUtils.INSTANCE.getPermGroupLabel(
170                         context, accessedPermGroupName).toString());
171             }
172         }
173 
174         TextView otherUseHeader = contentView.requireViewById(R.id.other_use_header);
175         TextView otherUseContent = contentView.requireViewById(R.id.other_use_content);
176 
177         boolean hasCallUsage = !callUsage.isEmpty();
178         boolean hasAppUsages = !appUsages.isEmpty();
179 
180         if (!hasCallUsage) {
181             otherUseHeader.setVisibility(View.GONE);
182             otherUseContent.setVisibility(View.GONE);
183         }
184 
185         if (!hasAppUsages) {
186             otherUseHeader.setVisibility(View.GONE);
187             appsList.setVisibility(View.GONE);
188         }
189 
190         if (!hasCallUsage) {
191             otherUseContent.setVisibility(View.GONE);
192         }
193 
194         if (hasCallUsage) {
195             if (callUsage.contains(VIDEO_CALL) && callUsage.contains(PHONE_CALL)) {
196                 otherUseContent.setText(
197                         Html.fromHtml(getString(R.string.phone_call_uses_microphone_and_camera),
198                                 0));
199             } else if (callUsage.contains(VIDEO_CALL)) {
200                 otherUseContent.setText(
201                         Html.fromHtml(getString(R.string.phone_call_uses_camera), 0));
202             } else if (callUsage.contains(PHONE_CALL)) {
203                 otherUseContent.setText(
204                         Html.fromHtml(getString(R.string.phone_call_uses_microphone), 0));
205             }
206 
207             if (callUsage.contains(VIDEO_CALL)) {
208                 usedGroups.put(CAMERA, KotlinUtils.INSTANCE.getPermGroupLabel(context, CAMERA));
209             }
210 
211             if (callUsage.contains(PHONE_CALL)) {
212                 usedGroups.put(MICROPHONE, KotlinUtils.INSTANCE.getPermGroupLabel(context,
213                         MICROPHONE));
214             }
215         }
216 
217         // Add the layout for each app.
218         for (Map.Entry<PackageAttribution, Set<String>> usage : appUsages.entrySet()) {
219             String packageName = usage.getKey().getPackageName();
220             UserHandle user = usage.getKey().getUser();
221 
222             Set<String> groups = usage.getValue();
223 
224             View itemView = inflater.inflate(R.layout.ongoing_usage_dialog_item, appsList, false);
225 
226             ((TextView) itemView.requireViewById(R.id.app_name))
227                     .setText(KotlinUtils.INSTANCE.getPackageLabel(context.getApplication(),
228                             packageName, user));
229             ((ImageView) itemView.requireViewById(R.id.app_icon))
230                     .setImageDrawable(KotlinUtils.INSTANCE.getBadgedPackageIcon(
231                             context.getApplication(), packageName, user));
232 
233             ArrayMap<String, CharSequence> usedGroupsThisApp = new ArrayMap<>();
234 
235             ViewGroup iconFrame = itemView.requireViewById(R.id.icons);
236             CharSequence specialGroupMessage = null;
237             for (String group : groups) {
238                 ViewGroup groupView = (ViewGroup) inflater.inflate(R.layout.image_view, null);
239                 ((ImageView) groupView.requireViewById(R.id.icon)).setImageDrawable(
240                         Utils.applyTint(context, KotlinUtils.INSTANCE.getPermGroupIcon(context,
241                                 group), android.R.attr.colorControlNormal));
242                 iconFrame.addView(groupView);
243 
244                 CharSequence groupLabel = KotlinUtils.INSTANCE.getPermGroupLabel(context, group);
245                 if (group.equals(MICROPHONE) && attrLabels.containsKey(usage.getKey())) {
246                     specialGroupMessage = ListFormatter.getInstance().format(
247                             attrLabels.get(usage.getKey()));
248                     continue;
249                 }
250 
251                 usedGroupsThisApp.put(group, groupLabel);
252             }
253             iconFrame.setVisibility(View.VISIBLE);
254 
255             TextView permissionsList = itemView.requireViewById(R.id.permissionsList);
256             permissionsList.setText(specialGroupMessage != null
257                     ? specialGroupMessage : getListOfPermissionLabels(usedGroupsThisApp));
258 
259             itemView.setOnClickListener((v) -> {
260                 PermissionControllerStatsLog.write(PRIVACY_INDICATORS_INTERACTED,
261                         PRIVACY_INDICATORS_INTERACTED__TYPE__DIALOG_LINE_ITEM);
262                 Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
263                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
264                 intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName);
265                 intent.putExtra(Intent.EXTRA_USER, user);
266                 context.startActivity(intent);
267                 mDialog.dismiss();
268             });
269 
270             appsList.addView(itemView);
271         }
272 
273         ((TextView) contentView.requireViewById(R.id.title)).setText(getTitle(usedGroups));
274 
275         return contentView;
276     }
277 
getTitle(ArrayMap<String, CharSequence> usedGroups)278     private CharSequence getTitle(ArrayMap<String, CharSequence> usedGroups) {
279         if (usedGroups.size() == 1 && usedGroups.keyAt(0).equals(MICROPHONE)) {
280             return getString(R.string.ongoing_usage_dialog_title_mic);
281         } else if (usedGroups.size() == 1 && usedGroups.keyAt(0).equals(CAMERA)) {
282             return getString(R.string.ongoing_usage_dialog_title_camera);
283         } else if (usedGroups.size() == 2 && usedGroups.containsKey(MICROPHONE)
284                 && usedGroups.containsKey(CAMERA)) {
285             return getString(R.string.ongoing_usage_dialog_title_mic_camera);
286         } else {
287             return getString(R.string.ongoing_usage_dialog_title,
288                     getListOfPermissionLabels(usedGroups));
289         }
290     }
291 
292     @Override
onCreatePreferences(Bundle bundle, String s)293     public void onCreatePreferences(Bundle bundle, String s) {
294         // empty
295     }
296 }
297