Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 85e7e33c authored by Joel Galenson's avatar Joel Galenson
Browse files

Improve ongoing usage dialog.

Fix a bug where the permission icons were not vertically centered.

Migrate the logic from the Activity to a Fragment to support TV.

Fix an issue where if you opened the dialog, followed it to another
screen, went to home, then re-opened the dialog, you would see the old
screen under it.

Test: Open dialog, click various options, re-open dialog.
Change-Id: I2dd1d195d32f8eb141d52c54d14f677f3d0b62bf
parent 7cad6231
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -145,6 +145,7 @@
        <activity android:name="com.android.packageinstaller.permission.ui.ReviewOngoingUsageActivity"
                  android:excludeFromRecents="true"
                  android:theme="@android:style/Theme.DeviceDefault.Light.Dialog.NoActionBar"
                  android:launchMode="singleInstance"
                  android:permission="android.permission.GRANT_RUNTIME_PERMISSIONS" >
            <intent-filter android:priority="1">
                <action android:name="android.intent.action.REVIEW_ONGOING_PERMISSION_USAGE" />
+2 −2
Original line number Diff line number Diff line
@@ -483,8 +483,8 @@
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">match_parent</item>
        <item name="android:orientation">horizontal</item>
        <item name="android:gravity">end</item>
        <item name="android:layout_gravity">end|center_vertical</item>
        <item name="android:gravity">end|center_vertical</item>
        <item name="android:layout_gravity">end</item>
        <item name="android:visibility">gone</item>
    </style>

+19 −165
Original line number Diff line number Diff line
@@ -16,40 +16,17 @@

package com.android.packageinstaller.permission.ui;

import static android.Manifest.permission_group.CAMERA;
import static android.Manifest.permission_group.LOCATION;
import static android.Manifest.permission_group.MICROPHONE;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;

import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArraySet;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.view.MenuItem;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;

import com.android.packageinstaller.permission.model.AppPermissionUsage;
import com.android.packageinstaller.permission.model.AppPermissionUsage.GroupUsage;
import com.android.packageinstaller.permission.model.PermissionApps;
import com.android.packageinstaller.permission.model.PermissionApps.PermissionApp;
import com.android.packageinstaller.permission.model.PermissionUsages;
import com.android.packageinstaller.permission.utils.Utils;
import com.android.permissioncontroller.R;

import java.text.Collator;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import com.android.packageinstaller.DeviceUtils;
import com.android.packageinstaller.permission.ui.handheld.ReviewOngoingUsageFragment;

/**
 * A dialog listing the currently uses of camera, microphone, and location.
@@ -59,154 +36,31 @@ public final class ReviewOngoingUsageActivity extends FragmentActivity {
    // Number of milliseconds in the past to look for accesses if nothing was specified.
    private static final long DEFAULT_MILLIS = 5000;

    private @NonNull PermissionUsages mPermissionUsages;
    private @Nullable AlertDialog mDialog;
    private long mStartTime;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (!Utils.isPermissionsHubEnabled()) {
            return;
        }
        getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);

        long numMillis = getIntent().getLongExtra(Intent.EXTRA_DURATION_MILLIS, DEFAULT_MILLIS);

        mPermissionUsages = new PermissionUsages(this);
        mStartTime = Math.max(System.currentTimeMillis() - numMillis, Instant.EPOCH.toEpochMilli());
        mPermissionUsages.load(null, new String[] { CAMERA, LOCATION, MICROPHONE }, mStartTime,
                Long.MAX_VALUE, PermissionUsages.USAGE_FLAG_LAST, getLoaderManager(), false, false,
                this::onPermissionUsagesLoaded, false);
        getSupportFragmentManager().beginTransaction().replace(android.R.id.content,
                ReviewOngoingUsageFragment.newInstance(numMillis)).commit();
    }

    private void onPermissionUsagesLoaded() {
        List<AppPermissionUsage> appPermissionUsages = mPermissionUsages.getUsages();

        List<Pair<AppPermissionUsage, List<GroupUsage>>> usages = new ArrayList<>();
        ArrayList<PermissionApp> permApps = new ArrayList<>();
        int numApps = appPermissionUsages.size();
        for (int appNum = 0; appNum < numApps; appNum++) {
            AppPermissionUsage appUsage = appPermissionUsages.get(appNum);

            List<GroupUsage> usedGroups = new ArrayList<>();
            List<GroupUsage> appGroups = appUsage.getGroupUsages();
            int numGroups = appGroups.size();
            for (int groupNum = 0; groupNum < numGroups; groupNum++) {
                GroupUsage groupUsage = appGroups.get(groupNum);
                String groupName = groupUsage.getGroup().getName();

                if (groupUsage.getLastAccessTime() < mStartTime && !groupUsage.isRunning()) {
                    continue;
                }
                if (!Utils.isGroupOrBgGroupUserSensitive(groupUsage.getGroup())) {
                    continue;
                }

                usedGroups.add(appGroups.get(groupNum));
            }

            if (!usedGroups.isEmpty()) {
                usages.add(Pair.create(appUsage, usedGroups));
                permApps.add(appUsage.getApp());
            }
        }

        if (usages.isEmpty()) {
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                // in automotive mode, there's no system wide back button, so need to add that
                if (DeviceUtils.isAuto(this)) {
                    onBackPressed();
                } else {
                    finish();
            return;
                }

        new PermissionApps.AppDataLoader(this, () -> showDialog(usages))
                .execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()]));
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }

    private void showDialog(@NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) {
        mDialog = new AlertDialog.Builder(this)
                .setView(createDialogView(usages))
                .setPositiveButton(R.string.ongoing_usage_dialog_ok, null)
                .setNeutralButton(R.string.ongoing_usage_dialog_open_settings, (dialog, which) ->
                      startActivity(new Intent(Settings.ACTION_PRIVACY_SETTINGS).putExtra(
                          Intent.EXTRA_DURATION_MILLIS, TimeUnit.MINUTES.toMillis(1))))
                .setOnDismissListener((dialog) -> finish())
                .create();
        mDialog.show();
    }

    private @NonNull View createDialogView(
            @NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) {
        LayoutInflater inflater = LayoutInflater.from(this);
        View contentView = inflater.inflate(R.layout.ongoing_usage_dialog_content, null);
        ViewGroup appsList = contentView.requireViewById(R.id.items_container);

        // Compute all of the permission group labels that were used.
        ArraySet<String> usedGroups = new ArraySet<>();
        int numUsages = usages.size();
        for (int usageNum = 0; usageNum < numUsages; usageNum++) {
            List<GroupUsage> groups = usages.get(usageNum).second;
            int numGroups = groups.size();
            for (int groupNum = 0; groupNum < numGroups; groupNum++) {
                usedGroups.add(groups.get(groupNum).getGroup().getLabel().toString().toLowerCase());
            }
        }

        // Add the layout for each app.
        for (int usageNum = 0; usageNum < numUsages; usageNum++) {
            Pair<AppPermissionUsage, List<GroupUsage>> usage = usages.get(usageNum);
            PermissionApp app = usage.first.getApp();
            List<GroupUsage> groups = usage.second;

            View itemView = inflater.inflate(R.layout.ongoing_usage_dialog_item, appsList, false);

            ((TextView) itemView.requireViewById(R.id.app_name)).setText(app.getLabel());
            ((ImageView) itemView.requireViewById(R.id.app_icon)).setImageDrawable(app.getIcon());

            // Add the icons for the groups this app used as long as multiple groups were used by
            // some app.
            if (usedGroups.size() > 1) {
                ViewGroup iconFrame = itemView.requireViewById(R.id.icons);
                int numGroups = usages.get(usageNum).second.size();
                for (int groupNum = 0; groupNum < numGroups; groupNum++) {
                    ViewGroup group = (ViewGroup) inflater.inflate(R.layout.image_view, null);
                    ((ImageView) group.requireViewById(R.id.icon)).setImageDrawable(
                            Utils.applyTint(this, groups.get(groupNum).getGroup().getIconResId(),
                                    android.R.attr.colorControlNormal));
                    iconFrame.addView(group);
                }
                iconFrame.setVisibility(View.VISIBLE);
            }

            itemView.setOnClickListener((v) -> {
                Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
                intent.putExtra(Intent.EXTRA_PACKAGE_NAME, app.getPackageName());
                intent.putExtra(Intent.EXTRA_USER, UserHandle.getUserHandleForUid(app.getUid()));
                startActivity(intent);
                mDialog.dismiss();
            });

            appsList.addView(itemView);
        }

        // Set the title of the dialog based on all of the permissions used.
        StringBuilder titleBuilder = new StringBuilder();
        int numGroups = usedGroups.size();
        List<String> sortedGroups = new ArrayList<>(usedGroups);
        Collator collator = Collator.getInstance(
                getResources().getConfiguration().getLocales().get(0));
        sortedGroups.sort(collator);
        for (int i = 0; i < numGroups; i++) {
            titleBuilder.append(sortedGroups.get(i));
            if (i < numGroups - 2) {
                titleBuilder.append(getString(R.string.ongoing_usage_dialog_separator));
            } else if (i < numGroups - 1) {
                titleBuilder.append(getString(R.string.ongoing_usage_dialog_last_separator));
            }
        }

        ((TextView) contentView.requireViewById(R.id.title)).setText(
                getString(R.string.ongoing_usage_dialog_title, titleBuilder.toString()));

        return contentView;
    }

}
+230 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.packageinstaller.permission.ui.handheld;

import static android.Manifest.permission_group.CAMERA;
import static android.Manifest.permission_group.LOCATION;
import static android.Manifest.permission_group.MICROPHONE;

import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArraySet;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceFragmentCompat;

import com.android.packageinstaller.permission.model.AppPermissionUsage;
import com.android.packageinstaller.permission.model.AppPermissionUsage.GroupUsage;
import com.android.packageinstaller.permission.model.PermissionApps;
import com.android.packageinstaller.permission.model.PermissionApps.PermissionApp;
import com.android.packageinstaller.permission.model.PermissionUsages;
import com.android.packageinstaller.permission.utils.Utils;
import com.android.permissioncontroller.R;

import java.text.Collator;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * A dialog listing the currently uses of camera, microphone, and location.
 */
public final class ReviewOngoingUsageFragment extends PreferenceFragmentCompat {

    private @NonNull PermissionUsages mPermissionUsages;
    private @Nullable AlertDialog mDialog;
    private long mStartTime;

    /**
     * @return A new {@link ReviewOngoingUsageFragment}
     */
    public static ReviewOngoingUsageFragment newInstance(long numMillis) {
        ReviewOngoingUsageFragment fragment = new ReviewOngoingUsageFragment();
        Bundle arguments = new Bundle();
        arguments.putLong(Intent.EXTRA_DURATION_MILLIS, numMillis);
        fragment.setArguments(arguments);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (!Utils.isPermissionsHubEnabled()) {
            return;
        }

        long numMillis = getArguments().getLong(Intent.EXTRA_DURATION_MILLIS);

        mPermissionUsages = new PermissionUsages(getActivity());
        mStartTime = Math.max(System.currentTimeMillis() - numMillis, Instant.EPOCH.toEpochMilli());
        mPermissionUsages.load(null, new String[] { CAMERA, LOCATION, MICROPHONE }, mStartTime,
                Long.MAX_VALUE, PermissionUsages.USAGE_FLAG_LAST, getActivity().getLoaderManager(),
                false, false, this::onPermissionUsagesLoaded, false);
    }

    private void onPermissionUsagesLoaded() {
        if (getActivity() == null) {
            return;
        }

        List<AppPermissionUsage> appPermissionUsages = mPermissionUsages.getUsages();

        List<Pair<AppPermissionUsage, List<GroupUsage>>> usages = new ArrayList<>();
        ArrayList<PermissionApp> permApps = new ArrayList<>();
        int numApps = appPermissionUsages.size();
        for (int appNum = 0; appNum < numApps; appNum++) {
            AppPermissionUsage appUsage = appPermissionUsages.get(appNum);

            List<GroupUsage> usedGroups = new ArrayList<>();
            List<GroupUsage> appGroups = appUsage.getGroupUsages();
            int numGroups = appGroups.size();
            for (int groupNum = 0; groupNum < numGroups; groupNum++) {
                GroupUsage groupUsage = appGroups.get(groupNum);
                String groupName = groupUsage.getGroup().getName();

                if (groupUsage.getLastAccessTime() < mStartTime && !groupUsage.isRunning()) {
                    continue;
                }
                if (!Utils.isGroupOrBgGroupUserSensitive(groupUsage.getGroup())) {
                    continue;
                }

                usedGroups.add(appGroups.get(groupNum));
            }

            if (!usedGroups.isEmpty()) {
                usages.add(Pair.create(appUsage, usedGroups));
                permApps.add(appUsage.getApp());
            }
        }

        if (usages.isEmpty()) {
            getActivity().finish();
            return;
        }

        new PermissionApps.AppDataLoader(getActivity(), () -> showDialog(usages))
                .execute(permApps.toArray(new PermissionApps.PermissionApp[permApps.size()]));
    }

    private void showDialog(@NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) {
        mDialog = new AlertDialog.Builder(getActivity())
                .setView(createDialogView(usages))
                .setPositiveButton(R.string.ongoing_usage_dialog_ok, null)
                .setNeutralButton(R.string.ongoing_usage_dialog_open_settings, (dialog, which) ->
                        startActivity(new Intent(Settings.ACTION_PRIVACY_SETTINGS).putExtra(
                                Intent.EXTRA_DURATION_MILLIS, TimeUnit.MINUTES.toMillis(1))))
                .setOnDismissListener((dialog) -> getActivity().finish())
                .create();
        mDialog.show();
    }

    private @NonNull View createDialogView(
            @NonNull List<Pair<AppPermissionUsage, List<GroupUsage>>> usages) {
        Context context = getActivity();
        LayoutInflater inflater = LayoutInflater.from(context);
        View contentView = inflater.inflate(R.layout.ongoing_usage_dialog_content, null);
        ViewGroup appsList = contentView.requireViewById(R.id.items_container);

        // Compute all of the permission group labels that were used.
        ArraySet<String> usedGroups = new ArraySet<>();
        int numUsages = usages.size();
        for (int usageNum = 0; usageNum < numUsages; usageNum++) {
            List<GroupUsage> groups = usages.get(usageNum).second;
            int numGroups = groups.size();
            for (int groupNum = 0; groupNum < numGroups; groupNum++) {
                usedGroups.add(groups.get(groupNum).getGroup().getLabel().toString().toLowerCase());
            }
        }

        // Add the layout for each app.
        for (int usageNum = 0; usageNum < numUsages; usageNum++) {
            Pair<AppPermissionUsage, List<GroupUsage>> usage = usages.get(usageNum);
            PermissionApp app = usage.first.getApp();
            List<GroupUsage> groups = usage.second;

            View itemView = inflater.inflate(R.layout.ongoing_usage_dialog_item, appsList, false);

            ((TextView) itemView.requireViewById(R.id.app_name)).setText(app.getLabel());
            ((ImageView) itemView.requireViewById(R.id.app_icon)).setImageDrawable(app.getIcon());

            // Add the icons for the groups this app used as long as multiple groups were used by
            // some app.
            if (usedGroups.size() > 1) {
                ViewGroup iconFrame = itemView.requireViewById(R.id.icons);
                int numGroups = usages.get(usageNum).second.size();
                for (int groupNum = 0; groupNum < numGroups; groupNum++) {
                    ViewGroup group = (ViewGroup) inflater.inflate(R.layout.image_view, null);
                    ((ImageView) group.requireViewById(R.id.icon)).setImageDrawable(
                            Utils.applyTint(context, groups.get(groupNum).getGroup().getIconResId(),
                                    android.R.attr.colorControlNormal));
                    iconFrame.addView(group);
                }
                iconFrame.setVisibility(View.VISIBLE);
            }

            itemView.setOnClickListener((v) -> {
                Intent intent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
                intent.putExtra(Intent.EXTRA_PACKAGE_NAME, app.getPackageName());
                intent.putExtra(Intent.EXTRA_USER, UserHandle.getUserHandleForUid(app.getUid()));
                startActivity(intent);
                mDialog.dismiss();
            });

            appsList.addView(itemView);
        }

        // Set the title of the dialog based on all of the permissions used.
        StringBuilder titleBuilder = new StringBuilder();
        int numGroups = usedGroups.size();
        List<String> sortedGroups = new ArrayList<>(usedGroups);
        Collator collator = Collator.getInstance(
                getResources().getConfiguration().getLocales().get(0));
        sortedGroups.sort(collator);
        for (int i = 0; i < numGroups; i++) {
            titleBuilder.append(sortedGroups.get(i));
            if (i < numGroups - 2) {
                titleBuilder.append(getString(R.string.ongoing_usage_dialog_separator));
            } else if (i < numGroups - 1) {
                titleBuilder.append(getString(R.string.ongoing_usage_dialog_last_separator));
            }
        }

        ((TextView) contentView.requireViewById(R.id.title)).setText(
                getString(R.string.ongoing_usage_dialog_title, titleBuilder.toString()));

        return contentView;
    }

    @Override
    public void onCreatePreferences(Bundle bundle, String s) {
        // empty
    }
}