diff --git a/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml b/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml new file mode 100644 index 0000000000000..918abd962dc2f --- /dev/null +++ b/packages/CarSystemUI/res/layout/car_ongoing_privacy_chip.xml @@ -0,0 +1,37 @@ + + + + + + + \ No newline at end of file diff --git a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml index 925ccb4a162a1..cae89c16a03ee 100644 --- a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml +++ b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml @@ -65,6 +65,8 @@ /> + + 24dp 96dp 128dp + 36dp + 0dp + 0dp + 0dp + 0dp + + 0dp + 0dp + 0dp + 0dp 100dp @@ -76,6 +86,16 @@ 4dp 12dp + + 40dp + + 40dp + + 20dp + + 10dp + + 10dp @*android:dimen/car_single_line_list_item_height diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java new file mode 100644 index 0000000000000..ead1de2bd352e --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/OngoingPrivacyChip.java @@ -0,0 +1,228 @@ +/* + * 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.systemui.statusbar.car.privacy; + +import android.app.ActivityManager; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import com.android.systemui.Dependency; +import com.android.systemui.R; +import com.android.systemui.appops.AppOpItem; +import com.android.systemui.appops.AppOpsController; +import com.android.systemui.plugins.ActivityStarter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Layout defining the privacy chip that will be displayed in CarStatusRar with the information for + * which applications are using AppOpps permission fpr camera, mic and location. + */ +public class OngoingPrivacyChip extends LinearLayout implements View.OnClickListener { + + private Context mContext; + + private LinearLayout mIconsContainer; + private List mPrivacyItems; + private static AppOpsController sAppOpsController; + private UserManager mUserManager; + private int mCurrentUser; + private List mCurrentUserIds; + private boolean mListening = false; + PrivacyDialogBuilder mPrivacyDialogBuilder; + private LinearLayout mPrivacyChip; + private ActivityStarter mActivityStarter; + + protected static final int[] OPS = new int[]{ + AppOpsManager.OP_CAMERA, + AppOpsManager.OP_RECORD_AUDIO, + AppOpsManager.OP_COARSE_LOCATION, + AppOpsManager.OP_FINE_LOCATION + }; + + public OngoingPrivacyChip(Context context) { + super(context, null); + init(context); + } + + public OngoingPrivacyChip(Context context, AttributeSet attr) { + super(context, attr); + init(context); + } + + public OngoingPrivacyChip(Context context, AttributeSet attr, int defStyle) { + super(context, attr, defStyle); + init(context); + } + + public OngoingPrivacyChip(Context context, AttributeSet attr, int defStyle, int a) { + super(context, attr, defStyle, a); + init(context); + } + + private void init(Context context) { + mContext = context; + mPrivacyItems = new ArrayList<>(); + sAppOpsController = Dependency.get(AppOpsController.class); + mUserManager = mContext.getSystemService(UserManager.class); + mActivityStarter = Dependency.get(ActivityStarter.class); + mCurrentUser = ActivityManager.getCurrentUser(); + mCurrentUserIds = mUserManager.getProfiles(mCurrentUser).stream().map( + userInfo -> userInfo.id).collect(Collectors.toList()); + + mPrivacyDialogBuilder = new PrivacyDialogBuilder(context, mPrivacyItems); + } + + private AppOpsController.Callback mCallback = new AppOpsController.Callback() { + + @Override + public void onActiveStateChanged(int code, int uid, String packageName, boolean active) { + int userId = UserHandle.getUserId(uid); + if (mCurrentUserIds.contains(userId)) { + updatePrivacyList(); + } + } + }; + + @Override + public void onFinishInflate() { + mIconsContainer = findViewById(R.id.icons_container); + mPrivacyChip = (LinearLayout) findViewById(R.id.car_privacy_chip); + if (mPrivacyChip != null) { + mPrivacyChip.setOnClickListener(this); + setListening(true); + } + } + + @Override + public void onDetachedFromWindow() { + if (mPrivacyChip != null) { + setListening(false); + } + super.onDetachedFromWindow(); + } + + @Override + public void onClick(View v) { + updatePrivacyList(); + Handler mUiHandler = new Handler(Looper.getMainLooper()); + mUiHandler.post(() -> { + mActivityStarter.postStartActivityDismissingKeyguard( + new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0); + }); + } + + private void setListening(boolean listen) { + if (mListening == listen) { + return; + } + mListening = listen; + if (mListening) { + sAppOpsController.addCallback(OPS, mCallback); + updatePrivacyList(); + } else { + sAppOpsController.removeCallback(OPS, mCallback); + } + } + + private void updatePrivacyList() { + mPrivacyItems = mCurrentUserIds.stream() + .flatMap(item -> sAppOpsController.getActiveAppOpsForUser(item).stream()) + .filter(Objects::nonNull) + .map(item -> toPrivacyItem(item)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + mPrivacyDialogBuilder = new PrivacyDialogBuilder(mContext, mPrivacyItems); + + Handler refresh = new Handler(Looper.getMainLooper()); + refresh.post(new Runnable() { + @Override + public void run() { + updateView(); + } + }); + } + + private PrivacyItem toPrivacyItem(AppOpItem appOpItem) { + PrivacyType type; + switch (appOpItem.getCode()) { + case AppOpsManager.OP_CAMERA: + type = PrivacyType.TYPE_CAMERA; + break; + case AppOpsManager.OP_COARSE_LOCATION: + type = PrivacyType.TYPE_LOCATION; + break; + case AppOpsManager.OP_FINE_LOCATION: + type = PrivacyType.TYPE_LOCATION; + break; + case AppOpsManager.OP_RECORD_AUDIO: + type = PrivacyType.TYPE_MICROPHONE; + break; + default: + return null; + } + PrivacyApplication app = new PrivacyApplication(appOpItem.getPackageName(), mContext); + return new PrivacyItem(type, app, appOpItem.getTimeStarted()); + } + + // Should only be called if the mPrivacyDialogBuilder icons or app changed + private void updateView() { + if (mPrivacyItems.isEmpty()) { + mPrivacyChip.setVisibility(GONE); + return; + } + mPrivacyChip.setVisibility(VISIBLE); + setIcons(mPrivacyDialogBuilder); + + requestLayout(); + } + + private void setIcons(PrivacyDialogBuilder dialogBuilder) { + mIconsContainer.removeAllViews(); + dialogBuilder.generateIcons().forEach(item -> { + int size = mContext.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_icon_height); + ImageView image = new ImageView(mContext); + image.setImageDrawable(item); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(size, size); + + int leftPadding = mContext.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_icon_padding_left); + int topPadding = mContext.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_icon_padding_top); + int rightPadding = mContext.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_icon_padding_right); + int bottomPadding = mContext.getResources().getDimensionPixelSize( + R.dimen.privacy_chip_icon_padding_bottom); + image.setLayoutParams(layoutParams); + image.setPadding(leftPadding, topPadding, rightPadding, bottomPadding); + mIconsContainer.addView(image); + }); + } +} diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java new file mode 100644 index 0000000000000..5ec7a77cb305e --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyApplication.java @@ -0,0 +1,62 @@ +/* + * 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.systemui.statusbar.car.privacy; + +import android.car.userlib.CarUserManagerHelper; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.util.Log; + +/** + * Class to hold the data for the applications that are using the AppOps permissions. + */ +public class PrivacyApplication { + private static final String TAG = "PrivacyApplication"; + + private Drawable mIcon; + private String mApplicationName; + + public PrivacyApplication(String packageName, Context context) { + try { + CarUserManagerHelper carUserManagerHelper = new CarUserManagerHelper(context); + ApplicationInfo app = context.getPackageManager() + .getApplicationInfoAsUser(packageName, 0, + carUserManagerHelper.getCurrentForegroundUserId()); + mIcon = context.getPackageManager().getApplicationIcon(app); + mApplicationName = context.getPackageManager().getApplicationLabel(app).toString(); + } catch (PackageManager.NameNotFoundException e) { + mApplicationName = packageName; + Log.e(TAG, "Failed to to find package name", e); + } + } + + /** + * Gets the application name. + */ + public Drawable getIcon() { + return mIcon; + } + + /** + * Gets the application name. + */ + public String getApplicationName() { + return mApplicationName; + } +} diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java new file mode 100644 index 0000000000000..3b83e7cc0623c --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyDialogBuilder.java @@ -0,0 +1,59 @@ +/* + * 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.systemui.statusbar.car.privacy; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Helper class to build the {@link OngoingPrivacyDialog} + */ +public class PrivacyDialogBuilder { + + private Map> mItemsByType; + private PrivacyApplication mApplication; + private Context mContext; + + public PrivacyDialogBuilder(Context context, List itemsList) { + mContext = context; + mItemsByType = itemsList.stream().filter(Objects::nonNull).collect( + Collectors.groupingBy(PrivacyItem::getPrivacyType)); + List apps = itemsList.stream().filter(Objects::nonNull).map( + PrivacyItem::getPrivacyApplication).distinct().collect(Collectors.toList()); + mApplication = apps.size() == 1 ? apps.get(0) : null; + } + + /** + * Gets the icon id for all the {@link PrivacyItem} in the same order as of itemList. + */ + public List generateIcons() { + return mItemsByType.keySet().stream().map(item -> item.getIconId(mContext)).collect( + Collectors.toList()); + } + + /** + * Gets the application object. + */ + public PrivacyApplication getApplication() { + return mApplication; + } +} diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java new file mode 100644 index 0000000000000..fca137392d741 --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyItem.java @@ -0,0 +1,46 @@ +/* + * 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.systemui.statusbar.car.privacy; + +/** + * Class for holding the data of each privacy item displayed in {@link OngoingPrivacyDialog} + */ +public class PrivacyItem { + + private PrivacyType mPrivacyType; + private PrivacyApplication mPrivacyApplication; + + public PrivacyItem(PrivacyType privacyType, PrivacyApplication privacyApplication, + long timeStarted) { + this.mPrivacyType = privacyType; + this.mPrivacyApplication = privacyApplication; + } + + /** + * Gets the application object. + */ + public PrivacyApplication getPrivacyApplication() { + return mPrivacyApplication; + } + + /** + * Gets the privacy type for the application. + */ + public PrivacyType getPrivacyType() { + return mPrivacyType; + } +} diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java new file mode 100644 index 0000000000000..8955c87bbff4f --- /dev/null +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/privacy/PrivacyType.java @@ -0,0 +1,53 @@ +/* + * 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.systemui.statusbar.car.privacy; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import com.android.systemui.R; + +/** + * Enum for storing data for camera, mic and location. + */ +public enum PrivacyType { + TYPE_CAMERA(R.string.privacy_type_camera, com.android.internal.R.drawable.ic_camera), + TYPE_LOCATION(R.string.privacy_type_location, R.drawable.stat_sys_location), + TYPE_MICROPHONE(R.string.privacy_type_microphone, R.drawable.ic_mic_white); + + private int mNameId; + private int mIconId; + + PrivacyType(int nameId, int iconId) { + mNameId = nameId; + mIconId = iconId; + } + + /** + * Get the icon Id. + */ + public Drawable getIconId(Context context) { + return context.getResources().getDrawable(mIconId, null); + } + + /** + * Get the name Id. + */ + public String getNameId(Context context) { + return context.getResources().getString(mNameId); + } +}