summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/system-current.txt2
-rw-r--r--core/java/android/app/NotificationHistory.aidl19
-rw-r--r--core/java/android/app/NotificationHistory.java506
-rw-r--r--core/java/android/os/ServiceManagerNative.java4
-rw-r--r--core/java/android/os/UserManager.java41
-rw-r--r--core/java/android/view/SurfaceView.java4
-rw-r--r--core/java/com/android/internal/app/ChooserActivity.java976
-rw-r--r--core/java/com/android/internal/app/ChooserFlags.java38
-rw-r--r--core/java/com/android/internal/app/ChooserListAdapter.java539
-rw-r--r--core/java/com/android/internal/app/ResolverActivity.java1148
-rw-r--r--core/java/com/android/internal/app/ResolverListAdapter.java862
-rw-r--r--core/java/com/android/internal/app/ResolverListController.java3
-rw-r--r--core/java/com/android/internal/app/SimpleIconFactory.java2
-rw-r--r--core/java/com/android/internal/app/chooser/ChooserTargetInfo.java53
-rw-r--r--core/java/com/android/internal/app/chooser/DisplayResolveInfo.java182
-rw-r--r--core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java88
-rw-r--r--core/java/com/android/internal/app/chooser/SelectableTargetInfo.java316
-rw-r--r--core/java/com/android/internal/app/chooser/TargetInfo.java128
-rw-r--r--core/jni/fd_utils.cpp14
-rw-r--r--core/tests/coretests/src/android/app/NotificationHistoryTest.java232
-rw-r--r--core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java56
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java25
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java12
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java12
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java43
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java71
-rw-r--r--graphics/java/android/graphics/fonts/Font.java5
-rw-r--r--media/java/android/media/MediaRecorder.java5
-rw-r--r--media/jni/audioeffect/Visualizer.h3
-rw-r--r--native/android/system_fonts.cpp61
-rw-r--r--packages/CarSystemUI/Android.bp58
-rw-r--r--packages/CarSystemUI/res/values/config.xml15
-rw-r--r--packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java4
-rw-r--r--packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java83
-rw-r--r--packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarView.java19
-rw-r--r--packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationButton.java5
-rw-r--r--packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java150
-rw-r--r--packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserGridRecyclerView.java26
-rw-r--r--packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserIconProvider.java116
-rw-r--r--packages/CarSystemUI/tests/Android.mk88
-rw-r--r--packages/CarSystemUI/tests/AndroidManifest.xml40
-rw-r--r--packages/CarSystemUI/tests/AndroidTest.xml30
-rw-r--r--packages/CarSystemUI/tests/res/values/config.xml23
-rw-r--r--packages/CarSystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java214
-rw-r--r--packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java408
-rw-r--r--packages/SettingsLib/res/values-af/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-am/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ar/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-as/strings.xml2
-rw-r--r--packages/SettingsLib/res/values-az/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-b+sr+Latn/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-be/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-bg/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-bn/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-bs/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ca/strings.xml3
-rw-r--r--packages/SettingsLib/res/values-cs/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-da/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-de/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-el/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-en-rAU/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-en-rCA/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-en-rGB/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-en-rIN/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-en-rXC/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-es-rUS/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-es/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-et/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-eu/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-fa/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-fi/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-fr-rCA/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-fr/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-gl/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-gu/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-hi/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-hr/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-hu/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-hy/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-in/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-is/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-it/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-iw/strings.xml2
-rw-r--r--packages/SettingsLib/res/values-ja/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ka/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-kk/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-km/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-kn/strings.xml2
-rw-r--r--packages/SettingsLib/res/values-ko/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ky/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-lo/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-lt/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-lv/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-mk/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ml/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-mn/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-mr/strings.xml2
-rw-r--r--packages/SettingsLib/res/values-ms/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-my/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-nb/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ne/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-nl/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-or/strings.xml2
-rw-r--r--packages/SettingsLib/res/values-pa/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-pl/strings.xml2
-rw-r--r--packages/SettingsLib/res/values-pt-rBR/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-pt-rPT/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-pt/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ro/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ru/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-si/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-sk/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-sl/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-sq/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-sr/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-sv/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-sw/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ta/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-te/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-th/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-tl/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-tr/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-uk/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-ur/strings.xml2
-rw-r--r--packages/SettingsLib/res/values-uz/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-vi/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-zh-rCN/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-zh-rHK/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-zh-rTW/strings.xml1
-rw-r--r--packages/SettingsLib/res/values-zu/strings.xml1
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java465
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java356
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java224
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java108
-rw-r--r--services/core/java/com/android/server/audio/AudioDeviceBroker.java515
-rw-r--r--services/core/java/com/android/server/audio/AudioDeviceInventory.java3
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java70
-rw-r--r--services/core/java/com/android/server/audio/BtHelper.java30
-rw-r--r--services/core/java/com/android/server/notification/NotificationHistoryDatabase.java300
-rw-r--r--services/core/java/com/android/server/notification/NotificationHistoryFilter.java125
-rw-r--r--services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java321
-rw-r--r--services/core/java/com/android/server/pm/AppsFilter.java15
-rw-r--r--services/core/java/com/android/server/pm/PackageManagerService.java10
-rw-r--r--services/core/jni/com_android_server_VibratorService.cpp2
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java16
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java184
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryFilterTest.java159
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryProtoHelperTest.java297
-rw-r--r--services/usage/java/com/android/server/usage/TEST_MAPPING33
-rw-r--r--wifi/java/android/net/wifi/WifiConfiguration.java17
158 files changed, 7225 insertions, 2884 deletions
diff --git a/api/system-current.txt b/api/system-current.txt
index adf9e39ba7cb..e6a3e9bf3e8e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -3639,7 +3639,7 @@ package android.media {
public final class MediaRecorder.AudioSource {
field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) public static final int ECHO_REFERENCE = 1997; // 0x7cd
field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public static final int HOTWORD = 1999; // 0x7cf
- field public static final int RADIO_TUNER = 1998; // 0x7ce
+ field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) public static final int RADIO_TUNER = 1998; // 0x7ce
}
public class PlayerProxy {
diff --git a/core/java/android/app/NotificationHistory.aidl b/core/java/android/app/NotificationHistory.aidl
new file mode 100644
index 000000000000..8150e743335a
--- /dev/null
+++ b/core/java/android/app/NotificationHistory.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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 android.app;
+
+parcelable NotificationHistory; \ No newline at end of file
diff --git a/core/java/android/app/NotificationHistory.java b/core/java/android/app/NotificationHistory.java
new file mode 100644
index 000000000000..c35246b49395
--- /dev/null
+++ b/core/java/android/app/NotificationHistory.java
@@ -0,0 +1,506 @@
+/*
+ * 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 android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * @hide
+ */
+public final class NotificationHistory implements Parcelable {
+
+ /**
+ * A historical notification. Any new fields added here should also be added to
+ * {@link #readNotificationFromParcel} and
+ * {@link #writeNotificationToParcel(HistoricalNotification, Parcel, int)}.
+ */
+ public static final class HistoricalNotification {
+ private String mPackage;
+ private String mChannelName;
+ private String mChannelId;
+ private int mUid;
+ private @UserIdInt int mUserId;
+ private long mPostedTimeMs;
+ private String mTitle;
+ private String mText;
+ private Icon mIcon;
+
+ private HistoricalNotification() {}
+
+ public String getPackage() {
+ return mPackage;
+ }
+
+ public String getChannelName() {
+ return mChannelName;
+ }
+
+ public String getChannelId() {
+ return mChannelId;
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ public long getPostedTimeMs() {
+ return mPostedTimeMs;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public String getText() {
+ return mText;
+ }
+
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ public String getKey() {
+ return mPackage + "|" + mUid + "|" + mPostedTimeMs;
+ }
+
+ @Override
+ public String toString() {
+ return "HistoricalNotification{" +
+ "key='" + getKey() + '\'' +
+ ", mChannelName='" + mChannelName + '\'' +
+ ", mChannelId='" + mChannelId + '\'' +
+ ", mUserId=" + mUserId +
+ ", mTitle='" + mTitle + '\'' +
+ ", mText='" + mText + '\'' +
+ ", mIcon=" + mIcon +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ HistoricalNotification that = (HistoricalNotification) o;
+ boolean iconsAreSame = getIcon() == null && that.getIcon() == null
+ || (getIcon() != null && that.getIcon() != null
+ && getIcon().sameAs(that.getIcon()));
+ return getUid() == that.getUid() &&
+ getUserId() == that.getUserId() &&
+ getPostedTimeMs() == that.getPostedTimeMs() &&
+ Objects.equals(getPackage(), that.getPackage()) &&
+ Objects.equals(getChannelName(), that.getChannelName()) &&
+ Objects.equals(getChannelId(), that.getChannelId()) &&
+ Objects.equals(getTitle(), that.getTitle()) &&
+ Objects.equals(getText(), that.getText()) &&
+ iconsAreSame;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getPackage(), getChannelName(), getChannelId(), getUid(),
+ getUserId(),
+ getPostedTimeMs(), getTitle(), getText(), getIcon());
+ }
+
+ public static final class Builder {
+ private String mPackage;
+ private String mChannelName;
+ private String mChannelId;
+ private int mUid;
+ private @UserIdInt int mUserId;
+ private long mPostedTimeMs;
+ private String mTitle;
+ private String mText;
+ private Icon mIcon;
+
+ public Builder() {}
+
+ public Builder setPackage(String aPackage) {
+ mPackage = aPackage;
+ return this;
+ }
+
+ public Builder setChannelName(String channelName) {
+ mChannelName = channelName;
+ return this;
+ }
+
+ public Builder setChannelId(String channelId) {
+ mChannelId = channelId;
+ return this;
+ }
+
+ public Builder setUid(int uid) {
+ mUid = uid;
+ return this;
+ }
+
+ public Builder setUserId(int userId) {
+ mUserId = userId;
+ return this;
+ }
+
+ public Builder setPostedTimeMs(long postedTimeMs) {
+ mPostedTimeMs = postedTimeMs;
+ return this;
+ }
+
+ public Builder setTitle(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ public Builder setText(String text) {
+ mText = text;
+ return this;
+ }
+
+ public Builder setIcon(Icon icon) {
+ mIcon = icon;
+ return this;
+ }
+
+ public HistoricalNotification build() {
+ HistoricalNotification n = new HistoricalNotification();
+ n.mPackage = mPackage;
+ n.mChannelName = mChannelName;
+ n.mChannelId = mChannelId;
+ n.mUid = mUid;
+ n.mUserId = mUserId;
+ n.mPostedTimeMs = mPostedTimeMs;
+ n.mTitle = mTitle;
+ n.mText = mText;
+ n.mIcon = mIcon;
+ return n;
+ }
+ }
+ }
+
+ // Only used when creating the resulting history. Not used for reading/unparceling.
+ private List<HistoricalNotification> mNotificationsToWrite = new ArrayList<>();
+ // ditto
+ private Set<String> mStringsToWrite = new HashSet<>();
+
+ // Mostly used for reading/unparceling events.
+ private Parcel mParcel = null;
+ private int mHistoryCount;
+ private int mIndex = 0;
+
+ // Sorted array of commonly used strings to shrink the size of the parcel. populated from
+ // mStringsToWrite on write and the parcel on read.
+ private String[] mStringPool;
+
+ /**
+ * Construct the iterator from a parcel.
+ */
+ private NotificationHistory(Parcel in) {
+ byte[] bytes = in.readBlob();
+ Parcel data = Parcel.obtain();
+ data.unmarshall(bytes, 0, bytes.length);
+ data.setDataPosition(0);
+ mHistoryCount = data.readInt();
+ mIndex = data.readInt();
+ if (mHistoryCount > 0) {
+ mStringPool = data.createStringArray();
+
+ final int listByteLength = data.readInt();
+ final int positionInParcel = data.readInt();
+ mParcel = Parcel.obtain();
+ mParcel.setDataPosition(0);
+ mParcel.appendFrom(data, data.dataPosition(), listByteLength);
+ mParcel.setDataSize(mParcel.dataPosition());
+ mParcel.setDataPosition(positionInParcel);
+ }
+ }
+
+ /**
+ * Create an empty iterator.
+ */
+ public NotificationHistory() {
+ mHistoryCount = 0;
+ }
+
+ /**
+ * Returns whether or not there are more events to read using {@link #getNextNotification()}.
+ *
+ * @return true if there are more events, false otherwise.
+ */
+ public boolean hasNextNotification() {
+ return mIndex < mHistoryCount;
+ }
+
+ /**
+ * Retrieve the next {@link HistoricalNotification} from the collection and put the
+ * resulting data into {@code notificationOut}.
+ *
+ * @return The next {@link HistoricalNotification} or null if there are no more notifications.
+ */
+ public @Nullable HistoricalNotification getNextNotification() {
+ if (!hasNextNotification()) {
+ return null;
+ }
+
+ HistoricalNotification n = readNotificationFromParcel(mParcel);
+
+ mIndex++;
+ if (!hasNextNotification()) {
+ mParcel.recycle();
+ mParcel = null;
+ }
+ return n;
+ }
+
+ /**
+ * Adds all of the pooled strings that have been read from disk
+ */
+ public void addPooledStrings(@NonNull List<String> strings) {
+ mStringsToWrite.addAll(strings);
+ }
+
+ /**
+ * Builds the pooled strings from pending notifications. Useful if the pooled strings on
+ * disk contains strings that aren't relevant to the notifications in our collection.
+ */
+ public void poolStringsFromNotifications() {
+ mStringsToWrite.clear();
+ for (int i = 0; i < mNotificationsToWrite.size(); i++) {
+ final HistoricalNotification notification = mNotificationsToWrite.get(i);
+ mStringsToWrite.add(notification.getPackage());
+ mStringsToWrite.add(notification.getChannelName());
+ mStringsToWrite.add(notification.getChannelId());
+ }
+ }
+
+ /**
+ * Used when populating a history from disk; adds an historical notification.
+ */
+ public void addNotificationToWrite(@NonNull HistoricalNotification notification) {
+ if (notification == null) {
+ return;
+ }
+ mNotificationsToWrite.add(notification);
+ mHistoryCount++;
+ }
+
+ /**
+ * Removes a package's historical notifications and regenerates the string pool
+ */
+ public void removeNotificationsFromWrite(String packageName) {
+ for (int i = mNotificationsToWrite.size() - 1; i >= 0; i--) {
+ if (packageName.equals(mNotificationsToWrite.get(i).getPackage())) {
+ mNotificationsToWrite.remove(i);
+ }
+ }
+ poolStringsFromNotifications();
+ }
+
+ /**
+ * Gets pooled strings in order to write them to disk
+ */
+ public @NonNull String[] getPooledStringsToWrite() {
+ String[] stringsToWrite = mStringsToWrite.toArray(new String[]{});
+ Arrays.sort(stringsToWrite);
+ return stringsToWrite;
+ }
+
+ /**
+ * Gets the historical notifications in order to write them to disk
+ */
+ public @NonNull List<HistoricalNotification> getNotificationsToWrite() {
+ return mNotificationsToWrite;
+ }
+
+ /**
+ * Gets the number of notifications in the collection
+ */
+ public int getHistoryCount() {
+ return mHistoryCount;
+ }
+
+ private int findStringIndex(String str) {
+ final int index = Arrays.binarySearch(mStringPool, str);
+ if (index < 0) {
+ throw new IllegalStateException("String '" + str + "' is not in the string pool");
+ }
+ return index;
+ }
+
+ /**
+ * Writes a single notification to the parcel. Modify this when updating member variables of
+ * {@link HistoricalNotification}.
+ */
+ private void writeNotificationToParcel(HistoricalNotification notification, Parcel p,
+ int flags) {
+ final int packageIndex;
+ if (notification.mPackage != null) {
+ packageIndex = findStringIndex(notification.mPackage);
+ } else {
+ packageIndex = -1;
+ }
+
+ final int channelNameIndex;
+ if (notification.getChannelName() != null) {
+ channelNameIndex = findStringIndex(notification.getChannelName());
+ } else {
+ channelNameIndex = -1;
+ }
+
+ final int channelIdIndex;
+ if (notification.getChannelId() != null) {
+ channelIdIndex = findStringIndex(notification.getChannelId());
+ } else {
+ channelIdIndex = -1;
+ }
+
+ p.writeInt(packageIndex);
+ p.writeInt(channelNameIndex);
+ p.writeInt(channelIdIndex);
+ p.writeInt(notification.getUid());
+ p.writeInt(notification.getUserId());
+ p.writeLong(notification.getPostedTimeMs());
+ p.writeString(notification.getTitle());
+ p.writeString(notification.getText());
+ notification.getIcon().writeToParcel(p, flags);
+ }
+
+ /**
+ * Reads a single notification from the parcel. Modify this when updating member variables of
+ * {@link HistoricalNotification}.
+ */
+ private HistoricalNotification readNotificationFromParcel(Parcel p) {
+ HistoricalNotification.Builder notificationOut = new HistoricalNotification.Builder();
+ final int packageIndex = p.readInt();
+ if (packageIndex >= 0) {
+ notificationOut.mPackage = mStringPool[packageIndex];
+ } else {
+ notificationOut.mPackage = null;
+ }
+
+ final int channelNameIndex = p.readInt();
+ if (channelNameIndex >= 0) {
+ notificationOut.setChannelName(mStringPool[channelNameIndex]);
+ } else {
+ notificationOut.setChannelName(null);
+ }
+
+ final int channelIdIndex = p.readInt();
+ if (channelIdIndex >= 0) {
+ notificationOut.setChannelId(mStringPool[channelIdIndex]);
+ } else {
+ notificationOut.setChannelId(null);
+ }
+
+ notificationOut.setUid(p.readInt());
+ notificationOut.setUserId(p.readInt());
+ notificationOut.setPostedTimeMs(p.readLong());
+ notificationOut.setTitle(p.readString());
+ notificationOut.setText(p.readString());
+ notificationOut.setIcon(Icon.CREATOR.createFromParcel(p));
+
+ return notificationOut.build();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ Parcel data = Parcel.obtain();
+ data.writeInt(mHistoryCount);
+ data.writeInt(mIndex);
+ if (mHistoryCount > 0) {
+ mStringPool = getPooledStringsToWrite();
+ data.writeStringArray(mStringPool);
+
+ if (!mNotificationsToWrite.isEmpty()) {
+ // typically system_server to a process
+
+ // Write out the events
+ Parcel p = Parcel.obtain();
+ try {
+ p.setDataPosition(0);
+ for (int i = 0; i < mHistoryCount; i++) {
+ final HistoricalNotification notification = mNotificationsToWrite.get(i);
+ writeNotificationToParcel(notification, p, flags);
+ }
+
+ final int listByteLength = p.dataPosition();
+
+ // Write the total length of the data.
+ data.writeInt(listByteLength);
+
+ // Write our current position into the data.
+ data.writeInt(0);
+
+ // Write the data.
+ data.appendFrom(p, 0, listByteLength);
+ } finally {
+ p.recycle();
+ }
+
+ } else if (mParcel != null) {
+ // typically process to process as mNotificationsToWrite is not populated on
+ // unparcel.
+
+ // Write the total length of the data.
+ data.writeInt(mParcel.dataSize());
+
+ // Write out current position into the data.
+ data.writeInt(mParcel.dataPosition());
+
+ // Write the data.
+ data.appendFrom(mParcel, 0, mParcel.dataSize());
+ } else {
+ throw new IllegalStateException(
+ "Either mParcel or mNotificationsToWrite must not be null");
+ }
+ }
+ // Data can be too large for a transact. Write the data as a Blob, which will be written to
+ // ashmem if too large.
+ dest.writeBlob(data.marshall());
+ }
+
+ public static final @NonNull Creator<NotificationHistory> CREATOR
+ = new Creator<NotificationHistory>() {
+ @Override
+ public NotificationHistory createFromParcel(Parcel source) {
+ return new NotificationHistory(source);
+ }
+
+ @Override
+ public NotificationHistory[] newArray(int size) {
+ return new NotificationHistory[size];
+ }
+ };
+}
diff --git a/core/java/android/os/ServiceManagerNative.java b/core/java/android/os/ServiceManagerNative.java
index f641731fa08f..124b6c6f7377 100644
--- a/core/java/android/os/ServiceManagerNative.java
+++ b/core/java/android/os/ServiceManagerNative.java
@@ -86,6 +86,10 @@ class ServiceManagerProxy implements IServiceManager {
throw new RemoteException();
}
+ public boolean isDeclared(String name) throws RemoteException {
+ throw new RemoteException();
+ }
+
/**
* Same as mServiceManager but used by apps.
*
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 71b94ed41351..b7a3c8f2f3be 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1314,7 +1314,8 @@ public class UserManager {
}
/**
- * Returns whether switching users is currently allowed.
+ * Returns whether switching users is currently allowed for the user this process is running
+ * under.
* <p>
* Switching users is not allowed in the following cases:
* <li>the user is in a phone call</li>
@@ -1329,10 +1330,24 @@ public class UserManager {
android.Manifest.permission.MANAGE_USERS,
android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
public @UserSwitchabilityResult int getUserSwitchability() {
- final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
- mContext.getContentResolver(),
- Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
- final boolean systemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM);
+ return getUserSwitchability(Process.myUserHandle());
+ }
+
+ /**
+ * Returns whether switching users is currently allowed for the provided user.
+ * <p>
+ * Switching users is not allowed in the following cases:
+ * <li>the user is in a phone call</li>
+ * <li>{@link #DISALLOW_USER_SWITCH} is set</li>
+ * <li>system user hasn't been unlocked yet</li>
+ *
+ * @return A {@link UserSwitchabilityResult} flag indicating if the user is switchable.
+ * @hide
+ */
+ @RequiresPermission(allOf = {Manifest.permission.READ_PHONE_STATE,
+ android.Manifest.permission.MANAGE_USERS,
+ android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional = true)
+ public @UserSwitchabilityResult int getUserSwitchability(UserHandle userHandle) {
final TelephonyManager tm =
(TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
@@ -1340,12 +1355,22 @@ public class UserManager {
if (tm.getCallState() != TelephonyManager.CALL_STATE_IDLE) {
flags |= SWITCHABILITY_STATUS_USER_IN_CALL;
}
- if (hasUserRestriction(DISALLOW_USER_SWITCH)) {
+ if (hasUserRestriction(DISALLOW_USER_SWITCH, userHandle)) {
flags |= SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
}
- if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) {
- flags |= SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED;
+
+ // System User is always unlocked in Headless System User Mode, so ignore this flag
+ if (!isHeadlessSystemUserMode()) {
+ final boolean allowUserSwitchingWhenSystemUserLocked = Settings.Global.getInt(
+ mContext.getContentResolver(),
+ Settings.Global.ALLOW_USER_SWITCHING_WHEN_SYSTEM_USER_LOCKED, 0) != 0;
+ final boolean systemUserUnlocked = isUserUnlocked(UserHandle.SYSTEM);
+
+ if (!allowUserSwitchingWhenSystemUserLocked && !systemUserUnlocked) {
+ flags |= SWITCHABILITY_STATUS_SYSTEM_USER_LOCKED;
+ }
}
+
return flags;
}
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 2f0a4ebb84f8..59e9ed1512ee 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -712,6 +712,8 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
mSurfaceAlpha = 1f;
synchronized (mSurfaceControlLock) {
+ mSurface.release();
+
if (mRtHandlingPositionUpdates) {
mRtReleaseSurfaces = true;
return;
@@ -725,7 +727,6 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
mTmpTransaction.remove(mBackgroundControl);
mBackgroundControl = null;
}
- mSurface.release();
mTmpTransaction.apply();
}
}
@@ -1198,7 +1199,6 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall
mRtTransaction.remove(mBackgroundControl);
mSurfaceControl = null;
mBackgroundControl = null;
- mSurface.release();
}
mRtHandlingPositionUpdates = false;
}
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 3b82f186d80e..b1752a47ea93 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -45,8 +45,6 @@ import android.content.IntentSender.SendIntentException;
import android.content.ServiceConnection;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
-import android.content.pm.LabeledIntent;
-import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
@@ -61,9 +59,7 @@ import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.AnimatedVectorDrawable;
-import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
import android.metrics.LogMaker;
import android.net.Uri;
import android.os.AsyncTask;
@@ -85,7 +81,6 @@ import android.service.chooser.ChooserTarget;
import android.service.chooser.ChooserTargetService;
import android.service.chooser.IChooserTargetResult;
import android.service.chooser.IChooserTargetService;
-import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.HashedStringCache;
@@ -110,6 +105,14 @@ import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
+import com.android.internal.app.ResolverListAdapter.ViewHolder;
+import com.android.internal.app.chooser.ChooserTargetInfo;
+import com.android.internal.app.chooser.DisplayResolveInfo;
+import com.android.internal.app.chooser.NotSelectableTargetInfo;
+import com.android.internal.app.chooser.SelectableTargetInfo;
+import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator;
+import com.android.internal.app.chooser.TargetInfo;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.MetricsLogger;
@@ -124,7 +127,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.Collator;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -138,7 +140,9 @@ import java.util.Set;
* for example, those generated by @see android.content.Intent#createChooser(Intent, CharSequence).
*
*/
-public class ChooserActivity extends ResolverActivity {
+public class ChooserActivity extends ResolverActivity implements
+ ChooserListAdapter.ChooserListCommunicator,
+ SelectableTargetInfoCommunicator {
private static final String TAG = "ChooserActivity";
@@ -154,12 +158,6 @@ public class ChooserActivity extends ResolverActivity {
private static final boolean DEBUG = false;
- /**
- * If {@link #USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS} and this is set to true,
- * {@link AppPredictionManager} will be queried for direct share targets.
- */
- // TODO(b/123089490): Replace with system flag
- private static final boolean USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS = true;
private static final boolean USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES = true;
// TODO(b/123088566) Share these in a better way.
private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share";
@@ -167,24 +165,21 @@ public class ChooserActivity extends ResolverActivity {
private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20;
public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter";
+ @VisibleForTesting
+ public static final int LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS = 250;
+
private boolean mIsAppPredictorComponentAvailable;
private AppPredictor mAppPredictor;
private AppPredictor.Callback mAppPredictorCallback;
private Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache;
- /**
- * If set to true, use ShortcutManager to retrieve the matching direct share targets, instead of
- * binding to every ChooserTargetService implementation.
- */
- // TODO(b/121287573): Replace with a system flag (setprop?)
- private static final boolean USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS = true;
- private static final boolean USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS = true;
-
public static final int TARGET_TYPE_DEFAULT = 0;
public static final int TARGET_TYPE_CHOOSER_TARGET = 1;
public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3;
+ private static final boolean USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS = true;
+
@IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
TARGET_TYPE_DEFAULT,
TARGET_TYPE_CHOOSER_TARGET,
@@ -233,10 +228,6 @@ public class ChooserActivity extends ResolverActivity {
private int mCurrAvailableWidth = 0;
- /** {@link ChooserActivity#getBaseScore} */
- public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
- /** {@link ChooserActivity#getBaseScore} */
- public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment";
// TODO: Update to handle landscape instead of using static value
private static final int MAX_RANKED_TARGETS = 4;
@@ -246,14 +237,9 @@ public class ChooserActivity extends ResolverActivity {
private static final int MAX_LOG_RANK_POSITION = 12;
- @VisibleForTesting
- public static final int LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS = 250;
-
private static final int MAX_EXTRA_INITIAL_INTENTS = 2;
private static final int MAX_EXTRA_CHOOSER_TARGETS = 2;
- private boolean mListViewDataChanged = false;
-
@Retention(SOURCE)
@IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT})
private @interface ContentPreviewType {
@@ -266,9 +252,6 @@ public class ChooserActivity extends ResolverActivity {
private static final int CONTENT_PREVIEW_TEXT = 3;
protected MetricsLogger mMetricsLogger;
- // Sorted list of DisplayResolveInfos for the alphabetical app section.
- private List<ResolverActivity.DisplayResolveInfo> mSortedList = new ArrayList<>();
-
private ContentPreviewCoordinator mPreviewCoord;
private class ContentPreviewCoordinator {
@@ -645,8 +628,7 @@ public class ChooserActivity extends ResolverActivity {
if (isFinishing() || isDestroyed()) {
return;
}
- // May be null if there are no apps to perform share/open action.
- if (mChooserListAdapter == null) {
+ if (mChooserListAdapter.getCount() == 0) {
return;
}
if (resultList.isEmpty()) {
@@ -775,7 +757,7 @@ public class ChooserActivity extends ResolverActivity {
@Override
public void onSomePackagesChanged() {
mAdapter.handlePackagesChanged();
- bindProfileView();
+ updateProfileViewButton();
}
};
}
@@ -1191,7 +1173,7 @@ public class ChooserActivity extends ResolverActivity {
}
}
- @Override
+ @Override // ResolverListCommunicator
public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
Intent result = defIntent;
if (mReplacementExtras != null) {
@@ -1231,9 +1213,8 @@ public class ChooserActivity extends ResolverActivity {
}
@Override
- public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) {
+ public void onPrepareAdapterView(AbsListView adapterView, ResolverListAdapter adapter) {
final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null;
- mChooserListAdapter = (ChooserListAdapter) adapter;
if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) {
mChooserListAdapter.addServiceResults(null, Lists.newArrayList(mCallerChooserTargets),
TARGET_TYPE_DEFAULT);
@@ -1245,11 +1226,17 @@ public class ChooserActivity extends ResolverActivity {
}
@Override
+ protected boolean rebuildList() {
+ mChooserListAdapter = (ChooserListAdapter) mAdapter;
+ return rebuildListInternal();
+ }
+
+ @Override
public int getLayoutResource() {
return R.layout.chooser_grid;
}
- @Override
+ @Override // ResolverListCommunicator
public boolean shouldGetActivityMetadata() {
return true;
}
@@ -1328,7 +1315,7 @@ public class ChooserActivity extends ResolverActivity {
final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
super.startSelected(which, always, filtered);
- if (mChooserListAdapter != null) {
+ if (mChooserListAdapter.getCount() > 0) {
// Log the index of which type of target the user picked.
// Lower values mean the ranking was better.
int cat = 0;
@@ -1342,7 +1329,7 @@ public class ChooserActivity extends ResolverActivity {
// Log the package name + target name to answer the question if most users
// share to mostly the same person or to a bunch of different people.
ChooserTarget target =
- mChooserListAdapter.mServiceTargets.get(value).getChooserTarget();
+ mChooserListAdapter.getChooserTargetForValue(value);
directTargetHashed = HashedStringCache.getInstance().hashString(
this,
TAG,
@@ -1428,7 +1415,7 @@ public class ChooserActivity extends ResolverActivity {
continue;
}
final ActivityInfo ai = dri.getResolveInfo().activityInfo;
- if (USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS
+ if (ChooserFlags.USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS
&& sm.hasShareTargets(ai.packageName)) {
// Share targets will be queried from ShortcutManager
continue;
@@ -1817,8 +1804,8 @@ public class ChooserActivity extends ResolverActivity {
*/
@Nullable
private AppPredictor getAppPredictorForDirectShareIfEnabled() {
- return USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS && !ActivityManager.isLowRamDeviceStatic()
- ? getAppPredictor() : null;
+ return ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS
+ && !ActivityManager.isLowRamDeviceStatic() ? getAppPredictor() : null;
}
/**
@@ -1900,24 +1887,18 @@ public class ChooserActivity extends ResolverActivity {
}
}
- private void updateAlphabeticalList() {
- mSortedList.clear();
- mSortedList.addAll(getDisplayList());
- Collections.sort(mSortedList, new AzInfoComparator(ChooserActivity.this));
- }
-
/**
* Sort intents alphabetically based on display label.
*/
- class AzInfoComparator implements Comparator<ResolverActivity.DisplayResolveInfo> {
+ static class AzInfoComparator implements Comparator<DisplayResolveInfo> {
Collator mCollator;
AzInfoComparator(Context context) {
mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
}
@Override
- public int compare(ResolverActivity.DisplayResolveInfo lhsp,
- ResolverActivity.DisplayResolveInfo rhsp) {
+ public int compare(
+ DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) {
return mCollator.compare(lhsp.getDisplayLabel(), rhsp.getDisplayLabel());
}
}
@@ -1955,12 +1936,12 @@ public class ChooserActivity extends ResolverActivity {
}
@Override
- public ResolveListAdapter createAdapter(Context context, List<Intent> payloadIntents,
- Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
- boolean filterLastUsed) {
- final ChooserListAdapter adapter = new ChooserListAdapter(context, payloadIntents,
- initialIntents, rList, launchedFromUid, filterLastUsed, createListController());
- return adapter;
+ public ResolverListAdapter createAdapter(Context context, List<Intent> payloadIntents,
+ Intent[] initialIntents, List<ResolveInfo> rList,
+ boolean filterLastUsed, boolean useLayoutForBrowsables) {
+ return new ChooserListAdapter(context, payloadIntents,
+ initialIntents, rList, filterLastUsed, createListController(),
+ useLayoutForBrowsables, this, this);
}
@VisibleForTesting
@@ -1999,345 +1980,19 @@ public class ChooserActivity extends ResolverActivity {
return null;
}
- interface ChooserTargetInfo extends TargetInfo {
- float getModifiedScore();
-
- ChooserTarget getChooserTarget();
-
- /**
- * Do not label as 'equals', since this doesn't quite work
- * as intended with java 8.
- */
- default boolean isSimilar(ChooserTargetInfo other) {
- if (other == null) return false;
-
- ChooserTarget ct1 = getChooserTarget();
- ChooserTarget ct2 = other.getChooserTarget();
-
- // If either is null, there is not enough info to make an informed decision
- // about equality, so just exit
- if (ct1 == null || ct2 == null) return false;
-
- if (ct1.getComponentName().equals(ct2.getComponentName())
- && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
- && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) {
- return true;
- }
-
- return false;
- }
- }
-
- /**
- * Distinguish between targets that selectable by the user, vs those that are
- * placeholders for the system while information is loading in an async manner.
- */
- abstract class NotSelectableTargetInfo implements ChooserTargetInfo {
-
- public Intent getResolvedIntent() {
- return null;
- }
-
- public ComponentName getResolvedComponentName() {
- return null;
- }
-
- public boolean start(Activity activity, Bundle options) {
- return false;
- }
-
- public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
- return false;
- }
-
- public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
- return false;
- }
-
- public ResolveInfo getResolveInfo() {
- return null;
- }
-
- public CharSequence getDisplayLabel() {
- return null;
- }
-
- public CharSequence getExtendedInfo() {
- return null;
- }
-
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return null;
- }
-
- public List<Intent> getAllSourceIntents() {
- return null;
- }
-
- public float getModifiedScore() {
- return -0.1f;
- }
-
- public ChooserTarget getChooserTarget() {
- return null;
- }
-
- public boolean isSuspended() {
- return false;
- }
- }
-
- final class PlaceHolderTargetInfo extends NotSelectableTargetInfo {
- public Drawable getDisplayIcon() {
+ static final class PlaceHolderTargetInfo extends NotSelectableTargetInfo {
+ public Drawable getDisplayIcon(Context context) {
AnimatedVectorDrawable avd = (AnimatedVectorDrawable)
- getDrawable(R.drawable.chooser_direct_share_icon_placeholder);
+ context.getDrawable(R.drawable.chooser_direct_share_icon_placeholder);
avd.start(); // Start animation after generation
return avd;
}
}
-
- final class EmptyTargetInfo extends NotSelectableTargetInfo {
- public Drawable getDisplayIcon() {
- return null;
- }
- }
-
- final class SelectableTargetInfo implements ChooserTargetInfo {
- private final DisplayResolveInfo mSourceInfo;
- private final ResolveInfo mBackupResolveInfo;
- private final ChooserTarget mChooserTarget;
- private final String mDisplayLabel;
- private Drawable mBadgeIcon = null;
- private CharSequence mBadgeContentDescription;
- private Drawable mDisplayIcon;
- private final Intent mFillInIntent;
- private final int mFillInFlags;
- private final float mModifiedScore;
- private boolean mIsSuspended = false;
-
- SelectableTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget,
- float modifiedScore) {
- mSourceInfo = sourceInfo;
- mChooserTarget = chooserTarget;
- mModifiedScore = modifiedScore;
- if (sourceInfo != null) {
- final ResolveInfo ri = sourceInfo.getResolveInfo();
- if (ri != null) {
- final ActivityInfo ai = ri.activityInfo;
- if (ai != null && ai.applicationInfo != null) {
- final PackageManager pm = getPackageManager();
- mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo);
- mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo);
- mIsSuspended =
- (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
- }
- }
- }
- // TODO(b/121287224): do this in the background thread, and only for selected targets
- mDisplayIcon = getChooserTargetIconDrawable(chooserTarget);
-
- if (sourceInfo != null) {
- mBackupResolveInfo = null;
- } else {
- mBackupResolveInfo = getPackageManager().resolveActivity(getResolvedIntent(), 0);
- }
-
- mFillInIntent = null;
- mFillInFlags = 0;
-
- mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle());
- }
-
- private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) {
- mSourceInfo = other.mSourceInfo;
- mBackupResolveInfo = other.mBackupResolveInfo;
- mChooserTarget = other.mChooserTarget;
- mBadgeIcon = other.mBadgeIcon;
- mBadgeContentDescription = other.mBadgeContentDescription;
- mDisplayIcon = other.mDisplayIcon;
- mFillInIntent = fillInIntent;
- mFillInFlags = flags;
- mModifiedScore = other.mModifiedScore;
-
- mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle());
- }
-
- private String sanitizeDisplayLabel(CharSequence label) {
- SpannableStringBuilder sb = new SpannableStringBuilder(label);
- sb.clearSpans();
- return sb.toString();
- }
-
- public boolean isSuspended() {
- return mIsSuspended;
- }
-
- /**
- * Since ShortcutInfos are returned by ShortcutManager, we can cache the shortcuts and skip
- * the call to LauncherApps#getShortcuts(ShortcutQuery).
- */
- // TODO(121287224): Refactor code to apply the suggestion above
- private Drawable getChooserTargetIconDrawable(ChooserTarget target) {
- Drawable directShareIcon = null;
-
- // First get the target drawable and associated activity info
- final Icon icon = target.getIcon();
- if (icon != null) {
- directShareIcon = icon.loadDrawable(ChooserActivity.this);
- } else if (USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS) {
- Bundle extras = target.getIntentExtras();
- if (extras != null && extras.containsKey(Intent.EXTRA_SHORTCUT_ID)) {
- CharSequence shortcutId = extras.getCharSequence(Intent.EXTRA_SHORTCUT_ID);
- LauncherApps launcherApps = (LauncherApps) getSystemService(
- Context.LAUNCHER_APPS_SERVICE);
- final LauncherApps.ShortcutQuery q = new LauncherApps.ShortcutQuery();
- q.setPackage(target.getComponentName().getPackageName());
- q.setShortcutIds(Arrays.asList(shortcutId.toString()));
- q.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC);
- final List<ShortcutInfo> shortcuts = launcherApps.getShortcuts(q, getUser());
- if (shortcuts != null && shortcuts.size() > 0) {
- directShareIcon = launcherApps.getShortcutIconDrawable(shortcuts.get(0), 0);
- }
- }
- }
-
- if (directShareIcon == null) return null;
-
- ActivityInfo info = null;
- try {
- info = mPm.getActivityInfo(target.getComponentName(), 0);
- } catch (NameNotFoundException error) {
- Log.e(TAG, "Could not find activity associated with ChooserTarget");
- }
-
- if (info == null) return null;
-
- // Now fetch app icon and raster with no badging even in work profile
- Bitmap appIcon = makePresentationGetter(info).getIconBitmap(
- UserHandle.getUserHandleForUid(UserHandle.myUserId()));
-
- // Raster target drawable with appIcon as a badge
- SimpleIconFactory sif = SimpleIconFactory.obtain(ChooserActivity.this);
- Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon);
- sif.recycle();
-
- return new BitmapDrawable(getResources(), directShareBadgedIcon);
- }
-
- public float getModifiedScore() {
- return mModifiedScore;
- }
-
- @Override
- public Intent getResolvedIntent() {
- if (mSourceInfo != null) {
- return mSourceInfo.getResolvedIntent();
- }
-
- final Intent targetIntent = new Intent(getTargetIntent());
- targetIntent.setComponent(mChooserTarget.getComponentName());
- targetIntent.putExtras(mChooserTarget.getIntentExtras());
- return targetIntent;
- }
-
- @Override
- public ComponentName getResolvedComponentName() {
- if (mSourceInfo != null) {
- return mSourceInfo.getResolvedComponentName();
- } else if (mBackupResolveInfo != null) {
- return new ComponentName(mBackupResolveInfo.activityInfo.packageName,
- mBackupResolveInfo.activityInfo.name);
- }
- return null;
- }
-
- private Intent getBaseIntentToSend() {
- Intent result = getResolvedIntent();
- if (result == null) {
- Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
- } else {
- result = new Intent(result);
- if (mFillInIntent != null) {
- result.fillIn(mFillInIntent, mFillInFlags);
- }
- result.fillIn(mReferrerFillInIntent, 0);
- }
- return result;
- }
-
- @Override
- public boolean start(Activity activity, Bundle options) {
- throw new RuntimeException("ChooserTargets should be started as caller.");
- }
-
- @Override
- public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
- final Intent intent = getBaseIntentToSend();
- if (intent == null) {
- return false;
- }
- intent.setComponent(mChooserTarget.getComponentName());
- intent.putExtras(mChooserTarget.getIntentExtras());
-
- // Important: we will ignore the target security checks in ActivityManager
- // if and only if the ChooserTarget's target package is the same package
- // where we got the ChooserTargetService that provided it. This lets a
- // ChooserTargetService provide a non-exported or permission-guarded target
- // to the chooser for the user to pick.
- //
- // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
- // so we'll obey the caller's normal security checks.
- final boolean ignoreTargetSecurity = mSourceInfo != null
- && mSourceInfo.getResolvedComponentName().getPackageName()
- .equals(mChooserTarget.getComponentName().getPackageName());
- return activity.startAsCallerImpl(intent, options, ignoreTargetSecurity, userId);
- }
-
- @Override
- public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
- throw new RuntimeException("ChooserTargets should be started as caller.");
- }
-
- @Override
- public ResolveInfo getResolveInfo() {
- return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
- }
-
- @Override
- public CharSequence getDisplayLabel() {
- return mDisplayLabel;
- }
-
- @Override
- public CharSequence getExtendedInfo() {
- // ChooserTargets have badge icons, so we won't show the extended info to disambiguate.
+ static final class EmptyTargetInfo extends NotSelectableTargetInfo {
+ public Drawable getDisplayIcon(Context context) {
return null;
}
-
- @Override
- public Drawable getDisplayIcon() {
- return mDisplayIcon;
- }
-
- public ChooserTarget getChooserTarget() {
- return mChooserTarget;
- }
-
- @Override
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return new SelectableTargetInfo(this, fillInIntent, flags);
- }
-
- @Override
- public List<Intent> getAllSourceIntents() {
- final List<Intent> results = new ArrayList<>();
- if (mSourceInfo != null) {
- // We only queried the service for the first one in our sourceinfo.
- results.add(mSourceInfo.getAllSourceIntents().get(0));
- }
- return results;
- }
}
private void handleScroll(View view, int x, int y, int oldx, int oldy) {
@@ -2408,7 +2063,8 @@ public class ChooserActivity extends ResolverActivity {
boolean isExpandable = getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode();
- if (directShareHeight != 0 && isSendAction(getTargetIntent()) && isExpandable) {
+ if (directShareHeight != 0 && isSendAction(getTargetIntent())
+ && isExpandable) {
// make sure to leave room for direct share 4->8 expansion
int requiredExpansionHeight =
(int) (directShareHeight / DIRECT_SHARE_EXPANSION_RATE);
@@ -2424,508 +2080,85 @@ public class ChooserActivity extends ResolverActivity {
}
}
- public class ChooserListAdapter extends ResolveListAdapter {
- public static final int TARGET_BAD = -1;
- public static final int TARGET_CALLER = 0;
- public static final int TARGET_SERVICE = 1;
- public static final int TARGET_STANDARD = 2;
- public static final int TARGET_STANDARD_AZ = 3;
-
- private static final int MAX_SUGGESTED_APP_TARGETS = 4;
- private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
-
- private static final int MAX_SERVICE_TARGETS = 8;
-
- private final int mMaxShortcutTargetsPerApp =
- getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
-
- private int mNumShortcutResults = 0;
-
- // Reserve spots for incoming direct share targets by adding placeholders
- private ChooserTargetInfo mPlaceHolderTargetInfo = new PlaceHolderTargetInfo();
- private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
- private final List<TargetInfo> mCallerTargets = new ArrayList<>();
-
- private final BaseChooserTargetComparator mBaseTargetComparator
- = new BaseChooserTargetComparator();
-
- public ChooserListAdapter(Context context, List<Intent> payloadIntents,
- Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
- boolean filterLastUsed, ResolverListController resolverListController) {
- // Don't send the initial intents through the shared ResolverActivity path,
- // we want to separate them into a different section.
- super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed,
- resolverListController);
-
- createPlaceHolders();
-
- if (initialIntents != null) {
- final PackageManager pm = getPackageManager();
- for (int i = 0; i < initialIntents.length; i++) {
- final Intent ii = initialIntents[i];
- if (ii == null) {
- continue;
- }
-
- // We reimplement Intent#resolveActivityInfo here because if we have an
- // implicit intent, we want the ResolveInfo returned by PackageManager
- // instead of one we reconstruct ourselves. The ResolveInfo returned might
- // have extra metadata and resolvePackageName set and we want to respect that.
- ResolveInfo ri = null;
- ActivityInfo ai = null;
- final ComponentName cn = ii.getComponent();
- if (cn != null) {
- try {
- ai = pm.getActivityInfo(ii.getComponent(), 0);
- ri = new ResolveInfo();
- ri.activityInfo = ai;
- } catch (PackageManager.NameNotFoundException ignored) {
- // ai will == null below
- }
- }
- if (ai == null) {
- ri = pm.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY);
- ai = ri != null ? ri.activityInfo : null;
- }
- if (ai == null) {
- Log.w(TAG, "No activity found for " + ii);
- continue;
- }
- UserManager userManager =
- (UserManager) getSystemService(Context.USER_SERVICE);
- if (ii instanceof LabeledIntent) {
- LabeledIntent li = (LabeledIntent) ii;
- ri.resolvePackageName = li.getSourcePackage();
- ri.labelRes = li.getLabelResource();
- ri.nonLocalizedLabel = li.getNonLocalizedLabel();
- ri.icon = li.getIconResource();
- ri.iconResourceId = ri.icon;
- }
- if (userManager.isManagedProfile()) {
- ri.noResourceId = true;
- ri.icon = 0;
- }
- mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii));
- }
- }
- }
-
- @Override
- public void handlePackagesChanged() {
- if (DEBUG) {
- Log.d(TAG, "clearing queryTargets on package change");
- }
- createPlaceHolders();
- mServicesRequested.clear();
- notifyDataSetChanged();
-
- super.handlePackagesChanged();
- }
-
+ static class BaseChooserTargetComparator implements Comparator<ChooserTarget> {
@Override
- public void notifyDataSetChanged() {
- if (!mListViewDataChanged) {
- mChooserHandler.sendEmptyMessageDelayed(ChooserHandler.LIST_VIEW_UPDATE_MESSAGE,
- LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS);
- mListViewDataChanged = true;
- }
+ public int compare(ChooserTarget lhs, ChooserTarget rhs) {
+ // Descending order
+ return (int) Math.signum(rhs.getScore() - lhs.getScore());
}
+ }
- private void refreshListView() {
- if (mListViewDataChanged) {
- super.notifyDataSetChanged();
- }
- mListViewDataChanged = false;
- }
+ @Override // ResolverListCommunicator
+ public void onHandlePackagesChanged() {
+ mServicesRequested.clear();
+ mAdapter.notifyDataSetChanged();
+ super.onHandlePackagesChanged();
+ }
+ @Override // SelectableTargetInfoCommunicator
+ public ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info) {
+ return mChooserListAdapter.makePresentationGetter(info);
+ }
- private void createPlaceHolders() {
- mNumShortcutResults = 0;
- mServiceTargets.clear();
- for (int i = 0; i < MAX_SERVICE_TARGETS; i++) {
- mServiceTargets.add(mPlaceHolderTargetInfo);
- }
- }
+ @Override // SelectableTargetInfoCommunicator
+ public Intent getReferrerFillInIntent() {
+ return mReferrerFillInIntent;
+ }
- @Override
- public View onCreateView(ViewGroup parent) {
- return mInflater.inflate(
- com.android.internal.R.layout.resolve_grid_item, parent, false);
- }
+ @Override // ChooserListCommunicator
+ public int getMaxRankedTargets() {
+ return mChooserRowAdapter == null ? 4 : mChooserRowAdapter.getMaxTargetsPerRow();
+ }
- @Override
- protected void onBindView(View view, TargetInfo info) {
- super.onBindView(view, info);
-
- // If target is loading, show a special placeholder shape in the label, make unclickable
- final ViewHolder holder = (ViewHolder) view.getTag();
- if (info instanceof PlaceHolderTargetInfo) {
- final int maxWidth = getResources().getDimensionPixelSize(
- R.dimen.chooser_direct_share_label_placeholder_max_width);
- holder.text.setMaxWidth(maxWidth);
- holder.text.setBackground(getResources().getDrawable(
- R.drawable.chooser_direct_share_label_placeholder, getTheme()));
- // Prevent rippling by removing background containing ripple
- holder.itemView.setBackground(null);
- } else {
- holder.text.setMaxWidth(Integer.MAX_VALUE);
- holder.text.setBackground(null);
- holder.itemView.setBackground(holder.defaultItemViewBackground);
- }
- }
+ @Override // ChooserListCommunicator
+ public void sendListViewUpdateMessage() {
+ mChooserHandler.sendEmptyMessageDelayed(ChooserHandler.LIST_VIEW_UPDATE_MESSAGE,
+ LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS);
+ }
- /**
- * Rather than fully sorting the input list, this sorting task will put the top k elements
- * in the head of input list and fill the tail with other elements in undetermined order.
- */
- @Override
- AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>> createSortingTask() {
- return new AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>>() {
+ @Override
+ public void onListRebuilt() {
+ if (mChooserListAdapter.mDisplayList == null
+ || mChooserListAdapter.mDisplayList.isEmpty()) {
+ mChooserListAdapter.notifyDataSetChanged();
+ } else {
+ new AsyncTask<Void, Void, Void>() {
@Override
- protected List<ResolvedComponentInfo> doInBackground(
- List<ResolvedComponentInfo>... params) {
- mResolverListController.topK(params[0],
- getMaxRankedTargets());
- return params[0];
+ protected Void doInBackground(Void... voids) {
+ mChooserListAdapter.updateAlphabeticalList();
+ return null;
}
-
@Override
- protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
- processSortedList(sortedComponents);
- bindProfileView();
- notifyDataSetChanged();
- }
- };
- }
-
- @Override
- public void onListRebuilt() {
- if (getDisplayList() == null || getDisplayList().isEmpty()) {
- notifyDataSetChanged();
- } else {
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... voids) {
- updateAlphabeticalList();
- return null;
- }
-
- @Override
- protected void onPostExecute(Void aVoid) {
- notifyDataSetChanged();
- }
- }.execute();
- }
-
- // don't support direct share on low ram devices
- if (ActivityManager.isLowRamDeviceStatic()) {
- return;
- }
-
- if (USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS
- || USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS) {
- if (DEBUG) {
- Log.d(TAG, "querying direct share targets from ShortcutManager");
- }
-
- queryDirectShareTargets(this, false);
- }
- if (USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS) {
- if (DEBUG) {
- Log.d(TAG, "List built querying services");
- }
-
- queryTargetServices(this);
- }
- }
-
- @Override
- public boolean shouldGetResolvedFilter() {
- return true;
- }
-
- @Override
- public int getCount() {
- return getRankedTargetCount() + getAlphaTargetCount()
- + getSelectableServiceTargetCount() + getCallerTargetCount();
- }
-
- @Override
- public int getUnfilteredCount() {
- int appTargets = super.getUnfilteredCount();
- if (appTargets > getMaxRankedTargets()) {
- appTargets = appTargets + getMaxRankedTargets();
- }
- return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
- }
-
-
- public int getCallerTargetCount() {
- return Math.min(mCallerTargets.size(), MAX_SUGGESTED_APP_TARGETS);
- }
-
- /**
- * Filter out placeholders and non-selectable service targets
- */
- public int getSelectableServiceTargetCount() {
- int count = 0;
- for (ChooserTargetInfo info : mServiceTargets) {
- if (info instanceof SelectableTargetInfo) {
- count++;
+ protected void onPostExecute(Void aVoid) {
+ mChooserListAdapter.notifyDataSetChanged();
}
- }
- return count;
- }
-
- public int getServiceTargetCount() {
- if (isSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) {
- return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS);
- }
-
- return 0;
+ }.execute();
}
- int getAlphaTargetCount() {
- int standardCount = super.getCount();
- return standardCount > getMaxRankedTargets() ? standardCount : 0;
- }
-
- int getRankedTargetCount() {
- int spacesAvailable = getMaxRankedTargets() - getCallerTargetCount();
- return Math.min(spacesAvailable, super.getCount());
- }
-
- private int getMaxRankedTargets() {
- return mChooserRowAdapter == null ? 4 : mChooserRowAdapter.getMaxTargetsPerRow();
- }
-
- public int getPositionTargetType(int position) {
- int offset = 0;
-
- final int serviceTargetCount = getServiceTargetCount();
- if (position < serviceTargetCount) {
- return TARGET_SERVICE;
- }
- offset += serviceTargetCount;
-
- final int callerTargetCount = getCallerTargetCount();
- if (position - offset < callerTargetCount) {
- return TARGET_CALLER;
- }
- offset += callerTargetCount;
-
- final int rankedTargetCount = getRankedTargetCount();
- if (position - offset < rankedTargetCount) {
- return TARGET_STANDARD;
- }
- offset += rankedTargetCount;
-
- final int standardTargetCount = getAlphaTargetCount();
- if (position - offset < standardTargetCount) {
- return TARGET_STANDARD_AZ;
- }
-
- return TARGET_BAD;
- }
-
- @Override
- public TargetInfo getItem(int position) {
- return targetInfoForPosition(position, true);
- }
-
-
- /**
- * Find target info for a given position.
- * Since ChooserActivity displays several sections of content, determine which
- * section provides this item.
- */
- @Override
- public TargetInfo targetInfoForPosition(int position, boolean filtered) {
- int offset = 0;
-
- // Direct share targets
- final int serviceTargetCount = filtered ? getServiceTargetCount() :
- getSelectableServiceTargetCount();
- if (position < serviceTargetCount) {
- return mServiceTargets.get(position);
- }
- offset += serviceTargetCount;
-
- // Targets provided by calling app
- final int callerTargetCount = getCallerTargetCount();
- if (position - offset < callerTargetCount) {
- return mCallerTargets.get(position - offset);
- }
- offset += callerTargetCount;
-
- // Ranked standard app targets
- final int rankedTargetCount = getRankedTargetCount();
- if (position - offset < rankedTargetCount) {
- return filtered ? super.getItem(position - offset)
- : getDisplayResolveInfo(position - offset);
- }
- offset += rankedTargetCount;
-
- // Alphabetical complete app target list.
- if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) {
- return mSortedList.get(position - offset);
- }
-
- return null;
+ // don't support direct share on low ram devices
+ if (ActivityManager.isLowRamDeviceStatic()) {
+ return;
}
-
- /**
- * Evaluate targets for inclusion in the direct share area. May not be included
- * if score is too low.
- */
- public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
- @ShareTargetType int targetType) {
+ if (ChooserFlags.USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS
+ || ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS) {
if (DEBUG) {
- Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size()
- + " targets");
- }
-
- if (targets.size() == 0) {
- return;
- }
-
- final float baseScore = getBaseScore(origTarget, targetType);
- Collections.sort(targets, mBaseTargetComparator);
-
- final boolean isShortcutResult =
- (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
- || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
- final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
- : MAX_CHOOSER_TARGETS_PER_APP;
- float lastScore = 0;
- boolean shouldNotify = false;
- for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) {
- final ChooserTarget target = targets.get(i);
- float targetScore = target.getScore();
- targetScore *= baseScore;
- if (i > 0 && targetScore >= lastScore) {
- // Apply a decay so that the top app can't crowd out everything else.
- // This incents ChooserTargetServices to define what's truly better.
- targetScore = lastScore * 0.95f;
- }
- boolean isInserted = insertServiceTarget(
- new SelectableTargetInfo(origTarget, target, targetScore));
-
- if (isInserted && isShortcutResult) {
- mNumShortcutResults++;
- }
-
- shouldNotify |= isInserted;
-
- if (DEBUG) {
- Log.d(TAG, " => " + target.toString() + " score=" + targetScore
- + " base=" + target.getScore()
- + " lastScore=" + lastScore
- + " baseScore=" + baseScore);
- }
-
- lastScore = targetScore;
+ Log.d(TAG, "querying direct share targets from ShortcutManager");
}
- if (shouldNotify) {
- notifyDataSetChanged();
- }
- }
-
- private int getNumShortcutResults() {
- return mNumShortcutResults;
+ queryDirectShareTargets(mChooserListAdapter, false);
}
-
- /**
- * Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
- * <ol>
- * <li>App-supplied targets
- * <li>Shortcuts ranked via App Prediction Manager
- * <li>Shortcuts ranked via legacy heuristics
- * <li>Legacy direct share targets
- * </ol>
- */
- public float getBaseScore(DisplayResolveInfo target, @ShareTargetType int targetType) {
- if (target == null) {
- return CALLER_TARGET_SCORE_BOOST;
- }
-
- if (targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
- return SHORTCUT_TARGET_SCORE_BOOST;
- }
-
- float score = super.getScore(target);
- if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
- return score * SHORTCUT_TARGET_SCORE_BOOST;
- }
-
- return score;
- }
-
- /**
- * Calling this marks service target loading complete, and will attempt to no longer
- * update the direct share area.
- */
- public void completeServiceTargetLoading() {
- mServiceTargets.removeIf(o -> o instanceof PlaceHolderTargetInfo);
-
- if (mServiceTargets.isEmpty()) {
- mServiceTargets.add(new EmptyTargetInfo());
- }
- notifyDataSetChanged();
- }
-
- private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
- // Avoid inserting any potentially late results
- if (mServiceTargets.size() == 1
- && mServiceTargets.get(0) instanceof EmptyTargetInfo) {
- return false;
- }
-
- // Check for duplicates and abort if found
- for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
- if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
- return false;
- }
- }
-
- int currentSize = mServiceTargets.size();
- final float newScore = chooserTargetInfo.getModifiedScore();
- for (int i = 0; i < Math.min(currentSize, MAX_SERVICE_TARGETS); i++) {
- final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
- if (serviceTarget == null) {
- mServiceTargets.set(i, chooserTargetInfo);
- return true;
- } else if (newScore > serviceTarget.getModifiedScore()) {
- mServiceTargets.add(i, chooserTargetInfo);
- return true;
- }
- }
-
- if (currentSize < MAX_SERVICE_TARGETS) {
- mServiceTargets.add(chooserTargetInfo);
- return true;
+ if (USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS) {
+ if (DEBUG) {
+ Log.d(TAG, "List built querying services");
}
- return false;
+ queryTargetServices(mChooserListAdapter);
}
}
- static class BaseChooserTargetComparator implements Comparator<ChooserTarget> {
- @Override
- public int compare(ChooserTarget lhs, ChooserTarget rhs) {
- // Descending order
- return (int) Math.signum(rhs.getScore() - lhs.getScore());
- }
- }
-
-
- private boolean isSendAction(Intent targetIntent) {
+ @Override // ChooserListCommunicator
+ public boolean isSendAction(Intent targetIntent) {
if (targetIntent == null) {
return false;
}
@@ -3080,7 +2313,8 @@ public class ChooserActivity extends ResolverActivity {
// There can be at most one row in the listview, that is internally
// a ViewGroup with 2 rows
public int getServiceTargetRowCount() {
- if (isSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) {
+ if (isSendAction(getTargetIntent())
+ && !ActivityManager.isLowRamDeviceStatic()) {
return 1;
}
return 0;
@@ -3177,7 +2411,7 @@ public class ChooserActivity extends ResolverActivity {
getResources().getDrawable(R.drawable.chooser_row_layer_list, null));
mProfileView = profileRow.findViewById(R.id.profile_button);
mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
- bindProfileView();
+ updateProfileViewButton();
return profileRow;
}
diff --git a/core/java/com/android/internal/app/ChooserFlags.java b/core/java/com/android/internal/app/ChooserFlags.java
new file mode 100644
index 000000000000..f1f1dbf49b8b
--- /dev/null
+++ b/core/java/com/android/internal/app/ChooserFlags.java
@@ -0,0 +1,38 @@
+/*
+ * 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.internal.app;
+
+import android.app.prediction.AppPredictionManager;
+
+/**
+ * Common flags for {@link ChooserListAdapter} and {@link ChooserActivity}.
+ */
+public class ChooserFlags {
+ /**
+ * If set to true, use ShortcutManager to retrieve the matching direct share targets, instead of
+ * binding to every ChooserTargetService implementation.
+ */
+ // TODO(b/121287573): Replace with a system flag (setprop?)
+ public static final boolean USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS = true;
+
+ /**
+ * If {@link ChooserFlags#USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS} and this is set to true,
+ * {@link AppPredictionManager} will be queried for direct share targets.
+ */
+ // TODO(b/123089490): Replace with system flag
+ static final boolean USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS = true;
+}
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
new file mode 100644
index 000000000000..6eb470fef2bc
--- /dev/null
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -0,0 +1,539 @@
+/*
+ * 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.internal.app;
+
+import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
+import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.LabeledIntent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.AsyncTask;
+import android.os.UserManager;
+import android.service.chooser.ChooserTarget;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
+import com.android.internal.app.chooser.ChooserTargetInfo;
+import com.android.internal.app.chooser.DisplayResolveInfo;
+import com.android.internal.app.chooser.SelectableTargetInfo;
+import com.android.internal.app.chooser.TargetInfo;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ChooserListAdapter extends ResolverListAdapter {
+ private static final String TAG = "ChooserListAdapter";
+ private static final boolean DEBUG = false;
+
+ public static final int TARGET_BAD = -1;
+ public static final int TARGET_CALLER = 0;
+ public static final int TARGET_SERVICE = 1;
+ public static final int TARGET_STANDARD = 2;
+ public static final int TARGET_STANDARD_AZ = 3;
+
+ private static final int MAX_SUGGESTED_APP_TARGETS = 4;
+ private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
+
+ static final int MAX_SERVICE_TARGETS = 8;
+
+ /** {@link #getBaseScore} */
+ public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
+ /** {@link #getBaseScore} */
+ public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
+
+ private final int mMaxShortcutTargetsPerApp;
+ private final ChooserListCommunicator mChooserListCommunicator;
+ private final SelectableTargetInfo.SelectableTargetInfoCommunicator
+ mSelectableTargetInfoComunicator;
+
+ private int mNumShortcutResults = 0;
+
+ // Reserve spots for incoming direct share targets by adding placeholders
+ private ChooserTargetInfo
+ mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
+ private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
+ private final List<TargetInfo> mCallerTargets = new ArrayList<>();
+
+ private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator =
+ new ChooserActivity.BaseChooserTargetComparator();
+ private boolean mListViewDataChanged = false;
+
+ // Sorted list of DisplayResolveInfos for the alphabetical app section.
+ private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
+
+ public ChooserListAdapter(Context context, List<Intent> payloadIntents,
+ Intent[] initialIntents, List<ResolveInfo> rList,
+ boolean filterLastUsed, ResolverListController resolverListController,
+ boolean useLayoutForBrowsables,
+ ChooserListCommunicator chooserListCommunicator,
+ SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoComunicator) {
+ // Don't send the initial intents through the shared ResolverActivity path,
+ // we want to separate them into a different section.
+ super(context, payloadIntents, null, rList, filterLastUsed,
+ resolverListController, useLayoutForBrowsables,
+ chooserListCommunicator);
+
+ createPlaceHolders();
+ mMaxShortcutTargetsPerApp =
+ context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
+ mChooserListCommunicator = chooserListCommunicator;
+ mSelectableTargetInfoComunicator = selectableTargetInfoComunicator;
+
+ if (initialIntents != null) {
+ final PackageManager pm = context.getPackageManager();
+ for (int i = 0; i < initialIntents.length; i++) {
+ final Intent ii = initialIntents[i];
+ if (ii == null) {
+ continue;
+ }
+
+ // We reimplement Intent#resolveActivityInfo here because if we have an
+ // implicit intent, we want the ResolveInfo returned by PackageManager
+ // instead of one we reconstruct ourselves. The ResolveInfo returned might
+ // have extra metadata and resolvePackageName set and we want to respect that.
+ ResolveInfo ri = null;
+ ActivityInfo ai = null;
+ final ComponentName cn = ii.getComponent();
+ if (cn != null) {
+ try {
+ ai = pm.getActivityInfo(ii.getComponent(), 0);
+ ri = new ResolveInfo();
+ ri.activityInfo = ai;
+ } catch (PackageManager.NameNotFoundException ignored) {
+ // ai will == null below
+ }
+ }
+ if (ai == null) {
+ ri = pm.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY);
+ ai = ri != null ? ri.activityInfo : null;
+ }
+ if (ai == null) {
+ Log.w(TAG, "No activity found for " + ii);
+ continue;
+ }
+ UserManager userManager =
+ (UserManager) context.getSystemService(Context.USER_SERVICE);
+ if (ii instanceof LabeledIntent) {
+ LabeledIntent li = (LabeledIntent) ii;
+ ri.resolvePackageName = li.getSourcePackage();
+ ri.labelRes = li.getLabelResource();
+ ri.nonLocalizedLabel = li.getNonLocalizedLabel();
+ ri.icon = li.getIconResource();
+ ri.iconResourceId = ri.icon;
+ }
+ if (userManager.isManagedProfile()) {
+ ri.noResourceId = true;
+ ri.icon = 0;
+ }
+ mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
+ }
+ }
+ }
+
+ @Override
+ public void handlePackagesChanged() {
+ if (DEBUG) {
+ Log.d(TAG, "clearing queryTargets on package change");
+ }
+ createPlaceHolders();
+ mChooserListCommunicator.onHandlePackagesChanged();
+
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ if (!mListViewDataChanged) {
+ mChooserListCommunicator.sendListViewUpdateMessage();
+ mListViewDataChanged = true;
+ }
+ }
+
+ void refreshListView() {
+ if (mListViewDataChanged) {
+ super.notifyDataSetChanged();
+ }
+ mListViewDataChanged = false;
+ }
+
+
+ private void createPlaceHolders() {
+ mNumShortcutResults = 0;
+ mServiceTargets.clear();
+ for (int i = 0; i < MAX_SERVICE_TARGETS; i++) {
+ mServiceTargets.add(mPlaceHolderTargetInfo);
+ }
+ }
+
+ @Override
+ public View onCreateView(ViewGroup parent) {
+ return mInflater.inflate(
+ com.android.internal.R.layout.resolve_grid_item, parent, false);
+ }
+
+ @Override
+ protected void onBindView(View view, TargetInfo info) {
+ super.onBindView(view, info);
+
+ // If target is loading, show a special placeholder shape in the label, make unclickable
+ final ViewHolder holder = (ViewHolder) view.getTag();
+ if (info instanceof ChooserActivity.PlaceHolderTargetInfo) {
+ final int maxWidth = mContext.getResources().getDimensionPixelSize(
+ R.dimen.chooser_direct_share_label_placeholder_max_width);
+ holder.text.setMaxWidth(maxWidth);
+ holder.text.setBackground(mContext.getResources().getDrawable(
+ R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme()));
+ // Prevent rippling by removing background containing ripple
+ holder.itemView.setBackground(null);
+ } else {
+ holder.text.setMaxWidth(Integer.MAX_VALUE);
+ holder.text.setBackground(null);
+ holder.itemView.setBackground(holder.defaultItemViewBackground);
+ }
+ }
+
+ void updateAlphabeticalList() {
+ mSortedList.clear();
+ mSortedList.addAll(mDisplayList);
+ Collections.sort(mSortedList, new ChooserActivity.AzInfoComparator(mContext));
+ }
+
+ @Override
+ public boolean shouldGetResolvedFilter() {
+ return true;
+ }
+
+ @Override
+ public int getCount() {
+ return getRankedTargetCount() + getAlphaTargetCount()
+ + getSelectableServiceTargetCount() + getCallerTargetCount();
+ }
+
+ @Override
+ public int getUnfilteredCount() {
+ int appTargets = super.getUnfilteredCount();
+ if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) {
+ appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets();
+ }
+ return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
+ }
+
+
+ public int getCallerTargetCount() {
+ return Math.min(mCallerTargets.size(), MAX_SUGGESTED_APP_TARGETS);
+ }
+
+ /**
+ * Filter out placeholders and non-selectable service targets
+ */
+ public int getSelectableServiceTargetCount() {
+ int count = 0;
+ for (ChooserTargetInfo info : mServiceTargets) {
+ if (info instanceof SelectableTargetInfo) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public int getServiceTargetCount() {
+ if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent())
+ && !ActivityManager.isLowRamDeviceStatic()) {
+ return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS);
+ }
+
+ return 0;
+ }
+
+ int getAlphaTargetCount() {
+ int standardCount = super.getCount();
+ return standardCount > mChooserListCommunicator.getMaxRankedTargets() ? standardCount : 0;
+ }
+
+ int getRankedTargetCount() {
+ int spacesAvailable =
+ mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount();
+ return Math.min(spacesAvailable, super.getCount());
+ }
+
+ public int getPositionTargetType(int position) {
+ int offset = 0;
+
+ final int serviceTargetCount = getServiceTargetCount();
+ if (position < serviceTargetCount) {
+ return TARGET_SERVICE;
+ }
+ offset += serviceTargetCount;
+
+ final int callerTargetCount = getCallerTargetCount();
+ if (position - offset < callerTargetCount) {
+ return TARGET_CALLER;
+ }
+ offset += callerTargetCount;
+
+ final int rankedTargetCount = getRankedTargetCount();
+ if (position - offset < rankedTargetCount) {
+ return TARGET_STANDARD;
+ }
+ offset += rankedTargetCount;
+
+ final int standardTargetCount = getAlphaTargetCount();
+ if (position - offset < standardTargetCount) {
+ return TARGET_STANDARD_AZ;
+ }
+
+ return TARGET_BAD;
+ }
+
+ @Override
+ public TargetInfo getItem(int position) {
+ return targetInfoForPosition(position, true);
+ }
+
+
+ /**
+ * Find target info for a given position.
+ * Since ChooserActivity displays several sections of content, determine which
+ * section provides this item.
+ */
+ @Override
+ public TargetInfo targetInfoForPosition(int position, boolean filtered) {
+ int offset = 0;
+
+ // Direct share targets
+ final int serviceTargetCount = filtered ? getServiceTargetCount() :
+ getSelectableServiceTargetCount();
+ if (position < serviceTargetCount) {
+ return mServiceTargets.get(position);
+ }
+ offset += serviceTargetCount;
+
+ // Targets provided by calling app
+ final int callerTargetCount = getCallerTargetCount();
+ if (position - offset < callerTargetCount) {
+ return mCallerTargets.get(position - offset);
+ }
+ offset += callerTargetCount;
+
+ // Ranked standard app targets
+ final int rankedTargetCount = getRankedTargetCount();
+ if (position - offset < rankedTargetCount) {
+ return filtered ? super.getItem(position - offset)
+ : getDisplayResolveInfo(position - offset);
+ }
+ offset += rankedTargetCount;
+
+ // Alphabetical complete app target list.
+ if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) {
+ return mSortedList.get(position - offset);
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Evaluate targets for inclusion in the direct share area. May not be included
+ * if score is too low.
+ */
+ public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
+ @ChooserActivity.ShareTargetType int targetType) {
+ if (DEBUG) {
+ Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size()
+ + " targets");
+ }
+
+ if (targets.size() == 0) {
+ return;
+ }
+
+ final float baseScore = getBaseScore(origTarget, targetType);
+ Collections.sort(targets, mBaseTargetComparator);
+
+ final boolean isShortcutResult =
+ (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
+ || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
+ final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
+ : MAX_CHOOSER_TARGETS_PER_APP;
+ float lastScore = 0;
+ boolean shouldNotify = false;
+ for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) {
+ final ChooserTarget target = targets.get(i);
+ float targetScore = target.getScore();
+ targetScore *= baseScore;
+ if (i > 0 && targetScore >= lastScore) {
+ // Apply a decay so that the top app can't crowd out everything else.
+ // This incents ChooserTargetServices to define what's truly better.
+ targetScore = lastScore * 0.95f;
+ }
+ boolean isInserted = insertServiceTarget(new SelectableTargetInfo(
+ mContext, origTarget, target, targetScore, mSelectableTargetInfoComunicator));
+
+ if (isInserted && isShortcutResult) {
+ mNumShortcutResults++;
+ }
+
+ shouldNotify |= isInserted;
+
+ if (DEBUG) {
+ Log.d(TAG, " => " + target.toString() + " score=" + targetScore
+ + " base=" + target.getScore()
+ + " lastScore=" + lastScore
+ + " baseScore=" + baseScore);
+ }
+
+ lastScore = targetScore;
+ }
+
+ if (shouldNotify) {
+ notifyDataSetChanged();
+ }
+ }
+
+ int getNumShortcutResults() {
+ return mNumShortcutResults;
+ }
+
+ /**
+ * Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
+ * <ol>
+ * <li>App-supplied targets
+ * <li>Shortcuts ranked via App Prediction Manager
+ * <li>Shortcuts ranked via legacy heuristics
+ * <li>Legacy direct share targets
+ * </ol>
+ */
+ public float getBaseScore(
+ DisplayResolveInfo target,
+ @ChooserActivity.ShareTargetType int targetType) {
+ if (target == null) {
+ return CALLER_TARGET_SCORE_BOOST;
+ }
+
+ if (targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
+ return SHORTCUT_TARGET_SCORE_BOOST;
+ }
+
+ float score = super.getScore(target);
+ if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
+ return score * SHORTCUT_TARGET_SCORE_BOOST;
+ }
+
+ return score;
+ }
+
+ /**
+ * Calling this marks service target loading complete, and will attempt to no longer
+ * update the direct share area.
+ */
+ public void completeServiceTargetLoading() {
+ mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
+
+ if (mServiceTargets.isEmpty()) {
+ mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
+ }
+ notifyDataSetChanged();
+ }
+
+ private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
+ // Avoid inserting any potentially late results
+ if (mServiceTargets.size() == 1
+ && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
+ return false;
+ }
+
+ // Check for duplicates and abort if found
+ for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
+ if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
+ return false;
+ }
+ }
+
+ int currentSize = mServiceTargets.size();
+ final float newScore = chooserTargetInfo.getModifiedScore();
+ for (int i = 0; i < Math.min(currentSize, MAX_SERVICE_TARGETS); i++) {
+ final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
+ if (serviceTarget == null) {
+ mServiceTargets.set(i, chooserTargetInfo);
+ return true;
+ } else if (newScore > serviceTarget.getModifiedScore()) {
+ mServiceTargets.add(i, chooserTargetInfo);
+ return true;
+ }
+ }
+
+ if (currentSize < MAX_SERVICE_TARGETS) {
+ mServiceTargets.add(chooserTargetInfo);
+ return true;
+ }
+
+ return false;
+ }
+
+ public ChooserTarget getChooserTargetForValue(int value) {
+ return mServiceTargets.get(value).getChooserTarget();
+ }
+
+ /**
+ * Rather than fully sorting the input list, this sorting task will put the top k elements
+ * in the head of input list and fill the tail with other elements in undetermined order.
+ */
+ @Override
+ AsyncTask<List<ResolvedComponentInfo>,
+ Void,
+ List<ResolvedComponentInfo>> createSortingTask() {
+ return new AsyncTask<List<ResolvedComponentInfo>,
+ Void,
+ List<ResolvedComponentInfo>>() {
+ @Override
+ protected List<ResolvedComponentInfo> doInBackground(
+ List<ResolvedComponentInfo>... params) {
+ mResolverListController.topK(params[0],
+ mChooserListCommunicator.getMaxRankedTargets());
+ return params[0];
+ }
+ @Override
+ protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
+ processSortedList(sortedComponents);
+ mChooserListCommunicator.updateProfileViewButton();
+ notifyDataSetChanged();
+ }
+ };
+ }
+
+ /**
+ * Necessary methods to communicate between {@link ChooserListAdapter}
+ * and {@link ChooserActivity}.
+ */
+ interface ChooserListCommunicator extends ResolverListCommunicator {
+
+ int getMaxRankedTargets();
+
+ void sendListViewUpdateMessage();
+
+ boolean isSendAction(Intent targetIntent);
+ }
+}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 74996e9fc212..c1c6ac9dfa89 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -23,7 +23,6 @@ import android.annotation.StringRes;
import android.annotation.UiThread;
import android.annotation.UnsupportedAppUsage;
import android.app.Activity;
-import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.ActivityThread;
import android.app.VoiceInteractor.PickOptionRequest;
@@ -35,26 +34,18 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
-import android.content.pm.LabeledIntent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
import android.graphics.Insets;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
import android.net.Uri;
-import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.PatternMatcher;
-import android.os.Process;
import android.os.RemoteException;
import android.os.StrictMode;
import android.os.UserHandle;
@@ -71,7 +62,6 @@ import android.view.ViewGroup.LayoutParams;
import android.view.WindowInsets;
import android.widget.AbsListView;
import android.widget.AdapterView;
-import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
@@ -81,6 +71,8 @@ import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.chooser.DisplayResolveInfo;
+import com.android.internal.app.chooser.TargetInfo;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
@@ -99,32 +91,33 @@ import java.util.Set;
* which to go to. It is not normally used directly by application developers.
*/
@UiThread
-public class ResolverActivity extends Activity {
-
- // Temporary flag for new chooser delegate behavior.
- boolean mEnableChooserDelegate = true;
+public class ResolverActivity extends Activity implements
+ ResolverListAdapter.ResolverListCommunicator {
@UnsupportedAppUsage
- protected ResolveListAdapter mAdapter;
+ protected ResolverListAdapter mAdapter;
private boolean mSafeForwardingMode;
protected AbsListView mAdapterView;
private Button mAlwaysButton;
private Button mOnceButton;
protected View mProfileView;
- private int mIconDpi;
private int mLastSelected = AbsListView.INVALID_POSITION;
private boolean mResolvingHome = false;
private int mProfileSwitchMessageId = -1;
private int mLayoutId;
- private final ArrayList<Intent> mIntents = new ArrayList<>();
+ @VisibleForTesting
+ protected final ArrayList<Intent> mIntents = new ArrayList<>();
private PickTargetOptionRequest mPickOptionRequest;
private String mReferrerPackage;
private CharSequence mTitle;
private int mDefaultTitleResId;
- private boolean mUseLayoutForBrowsables;
+
+ @VisibleForTesting
+ protected boolean mUseLayoutForBrowsables;
// Whether or not this activity supports choosing a default handler for the intent.
- private boolean mSupportsAlwaysUseOption;
+ @VisibleForTesting
+ protected boolean mSupportsAlwaysUseOption;
protected ResolverDrawerLayout mResolverDrawerLayout;
@UnsupportedAppUsage
protected PackageManager mPm;
@@ -132,12 +125,9 @@ public class ResolverActivity extends Activity {
private static final String TAG = "ResolverActivity";
private static final boolean DEBUG = false;
- private Runnable mPostListReadyRunnable;
private boolean mRegistered;
- private ColorMatrixColorFilter mSuspendedMatrixColorFilter;
-
protected Insets mSystemWindowInsets = null;
private Space mFooterSpacer = null;
@@ -233,7 +223,7 @@ public class ResolverActivity extends Activity {
@Override
public void onSomePackagesChanged() {
mAdapter.handlePackagesChanged();
- bindProfileView();
+ updateProfileViewButton();
}
@Override
@@ -316,9 +306,6 @@ public class ResolverActivity extends Activity {
mRegistered = true;
mReferrerPackage = getReferrerPackageName();
- final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
- mIconDpi = am.getLauncherLargeIconDensity();
-
// Add our initial intent as the first item, regardless of what else has already been added.
mIntents.add(0, new Intent(intent));
mTitle = title;
@@ -330,7 +317,17 @@ public class ResolverActivity extends Activity {
mSupportsAlwaysUseOption = supportsAlwaysUseOption;
- if (configureContentView(mIntents, initialIntents, rList)) {
+ // The last argument of createAdapter is whether to do special handling
+ // of the last used choice to highlight it in the list. We need to always
+ // turn this off when running under voice interaction, since it results in
+ // a more complicated UI that the current voice interaction flow is not able
+ // to handle.
+ boolean filterLastUsed = mSupportsAlwaysUseOption && !isVoiceInteraction();
+ mAdapter = createAdapter(this, mIntents, initialIntents, rList,
+ filterLastUsed, mUseLayoutForBrowsables);
+ configureContentView();
+
+ if (rebuildList()) {
return;
}
@@ -356,11 +353,9 @@ public class ResolverActivity extends Activity {
mProfileView = findViewById(R.id.profile_button);
if (mProfileView != null) {
mProfileView.setOnClickListener(this::onProfileClick);
- bindProfileView();
+ updateProfileViewButton();
}
- initSuspendedColorMatrix();
-
final Set<String> categories = intent.getCategories();
MetricsLogger.action(this, mAdapter.hasFilteredItem()
? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED
@@ -423,25 +418,7 @@ public class ResolverActivity extends Activity {
}
}
- private void initSuspendedColorMatrix() {
- int grayValue = 127;
- float scale = 0.5f; // half bright
-
- ColorMatrix tempBrightnessMatrix = new ColorMatrix();
- float[] mat = tempBrightnessMatrix.getArray();
- mat[0] = scale;
- mat[6] = scale;
- mat[12] = scale;
- mat[4] = grayValue;
- mat[9] = grayValue;
- mat[14] = grayValue;
-
- ColorMatrix matrix = new ColorMatrix();
- matrix.setSaturation(0.0f);
- matrix.preConcat(tempBrightnessMatrix);
- mSuspendedMatrixColorFilter = new ColorMatrixColorFilter(matrix);
- }
-
+ @Override // ResolverListCommunicator
public void sendVoiceChoicesIfNeeded() {
if (!isVoiceInteraction()) {
// Clearly not needed.
@@ -476,6 +453,7 @@ public class ResolverActivity extends Activity {
}
}
+ @Override // SelectableTargetInfoCommunicator ResolverListCommunicator
public Intent getTargetIntent() {
return mIntents.isEmpty() ? null : mIntents.get(0);
}
@@ -492,7 +470,8 @@ public class ResolverActivity extends Activity {
return R.layout.resolver_list;
}
- protected void bindProfileView() {
+ @Override // ResolverListCommunicator
+ public void updateProfileViewButton() {
if (mProfileView == null) {
return;
}
@@ -583,187 +562,6 @@ public class ResolverActivity extends Activity {
}
}
-
- /**
- * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application
- * icon and label over any IntentFilter or Activity icon to increase user understanding, with an
- * exception for applications that hold the right permission. Always attempts to use available
- * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
- * Strings to strip creative formatting.
- */
- private abstract static class TargetPresentationGetter {
- @Nullable abstract Drawable getIconSubstituteInternal();
- @Nullable abstract String getAppSubLabelInternal();
-
- private Context mCtx;
- private final int mIconDpi;
- private final boolean mHasSubstitutePermission;
- private final ApplicationInfo mAi;
-
- protected PackageManager mPm;
-
- TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) {
- mCtx = ctx;
- mPm = ctx.getPackageManager();
- mAi = ai;
- mIconDpi = iconDpi;
- mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission(
- android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON,
- mAi.packageName);
- }
-
- public Drawable getIcon(UserHandle userHandle) {
- return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle));
- }
-
- public Bitmap getIconBitmap(UserHandle userHandle) {
- Drawable dr = null;
- if (mHasSubstitutePermission) {
- dr = getIconSubstituteInternal();
- }
-
- if (dr == null) {
- try {
- if (mAi.icon != 0) {
- dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon);
- }
- } catch (NameNotFoundException ignore) {
- }
- }
-
- // Fall back to ApplicationInfo#loadIcon if nothing has been loaded
- if (dr == null) {
- dr = mAi.loadIcon(mPm);
- }
-
- SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx);
- Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle);
- sif.recycle();
-
- return icon;
- }
-
- public String getLabel() {
- String label = null;
- // Apps with the substitute permission will always show the sublabel as their label
- if (mHasSubstitutePermission) {
- label = getAppSubLabelInternal();
- }
-
- if (label == null) {
- label = (String) mAi.loadLabel(mPm);
- }
-
- return label;
- }
-
- public String getSubLabel() {
- // Apps with the substitute permission will never have a sublabel
- if (mHasSubstitutePermission) return null;
- return getAppSubLabelInternal();
- }
-
- protected String loadLabelFromResource(Resources res, int resId) {
- return res.getString(resId);
- }
-
- @Nullable
- protected Drawable loadIconFromResource(Resources res, int resId) {
- return res.getDrawableForDensity(resId, mIconDpi);
- }
-
- }
-
- /**
- * Loads the icon and label for the provided ResolveInfo.
- */
- @VisibleForTesting
- public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter {
- private final ResolveInfo mRi;
- public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) {
- super(ctx, iconDpi, ri.activityInfo);
- mRi = ri;
- }
-
- @Override
- Drawable getIconSubstituteInternal() {
- Drawable dr = null;
- try {
- // Do not use ResolveInfo#getIconResource() as it defaults to the app
- if (mRi.resolvePackageName != null && mRi.icon != 0) {
- dr = loadIconFromResource(
- mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon);
- }
- } catch (NameNotFoundException e) {
- Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
- + "couldn't find resources for package", e);
- }
-
- // Fall back to ActivityInfo if no icon is found via ResolveInfo
- if (dr == null) dr = super.getIconSubstituteInternal();
-
- return dr;
- }
-
- @Override
- String getAppSubLabelInternal() {
- // Will default to app name if no intent filter or activity label set, make sure to
- // check if subLabel matches label before final display
- return (String) mRi.loadLabel(mPm);
- }
- }
-
- ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) {
- return new ResolveInfoPresentationGetter(this, mIconDpi, ri);
- }
-
- /**
- * Loads the icon and label for the provided ActivityInfo.
- */
- @VisibleForTesting
- public static class ActivityInfoPresentationGetter extends TargetPresentationGetter {
- private final ActivityInfo mActivityInfo;
- public ActivityInfoPresentationGetter(Context ctx, int iconDpi,
- ActivityInfo activityInfo) {
- super(ctx, iconDpi, activityInfo.applicationInfo);
- mActivityInfo = activityInfo;
- }
-
- @Override
- Drawable getIconSubstituteInternal() {
- Drawable dr = null;
- try {
- // Do not use ActivityInfo#getIconResource() as it defaults to the app
- if (mActivityInfo.icon != 0) {
- dr = loadIconFromResource(
- mPm.getResourcesForApplication(mActivityInfo.applicationInfo),
- mActivityInfo.icon);
- }
- } catch (NameNotFoundException e) {
- Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
- + "couldn't find resources for package", e);
- }
-
- return dr;
- }
-
- @Override
- String getAppSubLabelInternal() {
- // Will default to app name if no activity label set, make sure to check if subLabel
- // matches label before final display
- return (String) mActivityInfo.loadLabel(mPm);
- }
- }
-
- protected ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) {
- return new ActivityInfoPresentationGetter(this, mIconDpi, ai);
- }
-
- Drawable loadIconForResolveInfo(ResolveInfo ri) {
- // Load icons based on the current process. If in work profile icons should be badged.
- return makePresentationGetter(ri).getIcon(Process.myUserHandle());
- }
-
@Override
protected void onRestart() {
super.onRestart();
@@ -772,7 +570,7 @@ public class ResolverActivity extends Activity {
mRegistered = true;
}
mAdapter.handlePackagesChanged();
- bindProfileView();
+ updateProfileViewButton();
}
@Override
@@ -804,12 +602,8 @@ public class ResolverActivity extends Activity {
if (!isChangingConfigurations() && mPickOptionRequest != null) {
mPickOptionRequest.cancel();
}
- if (mPostListReadyRunnable != null) {
- getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
- mPostListReadyRunnable = null;
- }
- if (mAdapter != null && mAdapter.mResolverListController != null) {
- mAdapter.mResolverListController.destroy();
+ if (mAdapter != null) {
+ mAdapter.onDestroy();
}
}
@@ -950,10 +744,30 @@ public class ResolverActivity extends Activity {
/**
* Replace me in subclasses!
*/
+ @Override // ResolverListCommunicator
public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
return defIntent;
}
+ @Override // ResolverListCommunicator
+ public void onPostListReady() {
+ setHeader();
+ resetButtonBar();
+ onListRebuilt();
+ }
+
+ protected void onListRebuilt() {
+ int count = mAdapter.getUnfilteredCount();
+ if (count == 1 && mAdapter.getOtherProfile() == null) {
+ // Only one target, so we're a candidate to auto-launch!
+ final TargetInfo target = mAdapter.targetInfoForPosition(0, false);
+ if (shouldAutoLaunchSingleChoice(target)) {
+ safelyStartActivity(target);
+ finish();
+ }
+ }
+ }
+
protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
final ResolveInfo ri = target.getResolveInfo();
final Intent intent = target != null ? target.getResolvedIntent() : null;
@@ -1049,8 +863,8 @@ public class ResolverActivity extends Activity {
// If we don't add back in the component for forwarding the intent to a managed
// profile, the preferred activity may not be updated correctly (as the set of
// components we tell it we knew about will have changed).
- final boolean needToAddBackProfileForwardingComponent
- = mAdapter.mOtherProfile != null;
+ final boolean needToAddBackProfileForwardingComponent =
+ mAdapter.getOtherProfile() != null;
if (!needToAddBackProfileForwardingComponent) {
set = new ComponentName[N];
} else {
@@ -1066,8 +880,8 @@ public class ResolverActivity extends Activity {
}
if (needToAddBackProfileForwardingComponent) {
- set[N] = mAdapter.mOtherProfile.getResolvedComponentName();
- final int otherProfileMatch = mAdapter.mOtherProfile.getResolveInfo().match;
+ set[N] = mAdapter.getOtherProfile().getResolvedComponentName();
+ final int otherProfileMatch = mAdapter.getOtherProfile().getResolveInfo().match;
if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch;
}
@@ -1169,7 +983,7 @@ public class ResolverActivity extends Activity {
}
- boolean startAsCallerImpl(Intent intent, Bundle options, boolean ignoreTargetSecurity,
+ public boolean startAsCallerImpl(Intent intent, Bundle options, boolean ignoreTargetSecurity,
int userId) {
// Pass intent to delegate chooser activity with permission token.
// TODO: This should move to a trampoline Activity in the system when the ChooserActivity
@@ -1205,6 +1019,7 @@ public class ResolverActivity extends Activity {
// Do nothing
}
+ @Override // ResolverListCommunicator
public boolean shouldGetActivityMetadata() {
return false;
}
@@ -1220,11 +1035,11 @@ public class ResolverActivity extends Activity {
startActivity(in);
}
- public ResolveListAdapter createAdapter(Context context, List<Intent> payloadIntents,
- Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
- boolean filterLastUsed) {
- return new ResolveListAdapter(context, payloadIntents, initialIntents, rList,
- launchedFromUid, filterLastUsed, createListController());
+ public ResolverListAdapter createAdapter(Context context, List<Intent> payloadIntents,
+ Intent[] initialIntents, List<ResolveInfo> rList,
+ boolean filterLastUsed, boolean useLayoutForBrowsables) {
+ return new ResolverListAdapter(context, payloadIntents, initialIntents, rList,
+ filterLastUsed, createListController(), useLayoutForBrowsables, this);
}
@VisibleForTesting
@@ -1238,26 +1053,34 @@ public class ResolverActivity extends Activity {
}
/**
- * Returns true if the activity is finishing and creation should halt
+ * Sets up the content view.
*/
- public boolean configureContentView(List<Intent> payloadIntents, Intent[] initialIntents,
- List<ResolveInfo> rList) {
- // The last argument of createAdapter is whether to do special handling
- // of the last used choice to highlight it in the list. We need to always
- // turn this off when running under voice interaction, since it results in
- // a more complicated UI that the current voice interaction flow is not able
- // to handle.
- mAdapter = createAdapter(this, payloadIntents, initialIntents, rList,
- mLaunchedFromUid, mSupportsAlwaysUseOption && !isVoiceInteraction());
- boolean rebuildCompleted = mAdapter.rebuildList();
-
+ private void configureContentView() {
+ if (mAdapter == null) {
+ throw new IllegalStateException("mAdapter cannot be null.");
+ }
if (useLayoutWithDefault()) {
mLayoutId = R.layout.resolver_list_with_default;
} else {
mLayoutId = getLayoutResource();
}
setContentView(mLayoutId);
+ mAdapterView = findViewById(R.id.resolver_list);
+ }
+
+ /**
+ * Returns true if the activity is finishing and creation should halt.
+ * </p>Subclasses must call rebuildListInternal at the end of rebuildList.
+ */
+ protected boolean rebuildList() {
+ return rebuildListInternal();
+ }
+ /**
+ * Returns true if the activity is finishing and creation should halt.
+ */
+ final boolean rebuildListInternal() {
+ boolean rebuildCompleted = mAdapter.rebuildList();
int count = mAdapter.getUnfilteredCount();
// We only rebuild asynchronously when we have multiple elements to sort. In the case where
@@ -1276,10 +1099,7 @@ public class ResolverActivity extends Activity {
}
}
-
- mAdapterView = findViewById(R.id.resolver_list);
-
- if (count == 0 && mAdapter.mPlaceholderCount == 0) {
+ if (count == 0 && mAdapter.getPlaceholderCount() == 0) {
final TextView emptyView = findViewById(R.id.empty);
emptyView.setVisibility(View.VISIBLE);
mAdapterView.setVisibility(View.GONE);
@@ -1290,7 +1110,7 @@ public class ResolverActivity extends Activity {
return false;
}
- public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) {
+ public void onPrepareAdapterView(AbsListView adapterView, ResolverListAdapter adapter) {
final boolean useHeader = adapter.hasFilteredItem();
final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null;
@@ -1317,7 +1137,7 @@ public class ResolverActivity extends Activity {
* Configure the area above the app selection list (title, content preview, etc).
*/
public void setHeader() {
- if (mAdapter.getCount() == 0 && mAdapter.mPlaceholderCount == 0) {
+ if (mAdapter.getCount() == 0 && mAdapter.getPlaceholderCount() == 0) {
final TextView titleView = findViewById(R.id.title);
if (titleView != null) {
titleView.setVisibility(View.GONE);
@@ -1337,9 +1157,8 @@ public class ResolverActivity extends Activity {
}
final ImageView iconView = findViewById(R.id.icon);
- final DisplayResolveInfo iconInfo = mAdapter.getFilteredItem();
- if (iconView != null && iconInfo != null) {
- new LoadIconTask(iconInfo, iconView).execute();
+ if (iconView != null) {
+ mAdapter.loadFilteredItemIconTaskAsync(iconView);
}
}
@@ -1382,7 +1201,8 @@ public class ResolverActivity extends Activity {
}
}
- private boolean useLayoutWithDefault() {
+ @Override // ResolverListCommunicator
+ public boolean useLayoutWithDefault() {
return mSupportsAlwaysUseOption && mAdapter.hasFilteredItem();
}
@@ -1397,706 +1217,22 @@ public class ResolverActivity extends Activity {
/**
* Check a simple match for the component of two ResolveInfos.
*/
- static boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) {
+ @Override // ResolverListCommunicator
+ public boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) {
return lhs == null ? rhs == null
: lhs.activityInfo == null ? rhs.activityInfo == null
: Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name)
&& Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName);
}
- public final class DisplayResolveInfo implements TargetInfo {
- private final ResolveInfo mResolveInfo;
- private CharSequence mDisplayLabel;
- private Drawable mDisplayIcon;
- private Drawable mBadge;
- private CharSequence mExtendedInfo;
- private final Intent mResolvedIntent;
- private final List<Intent> mSourceIntents = new ArrayList<>();
- private boolean mIsSuspended;
-
- public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent) {
- this(originalIntent, pri, null /*mDisplayLabel*/, null /*mExtendedInfo*/, pOrigIntent);
- }
-
- public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel,
- CharSequence pInfo, Intent pOrigIntent) {
- mSourceIntents.add(originalIntent);
- mResolveInfo = pri;
- mDisplayLabel = pLabel;
- mExtendedInfo = pInfo;
-
- final Intent intent = new Intent(pOrigIntent != null ? pOrigIntent :
- getReplacementIntent(pri.activityInfo, getTargetIntent()));
- intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
- | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
- final ActivityInfo ai = mResolveInfo.activityInfo;
- intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
-
- mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
-
- mResolvedIntent = intent;
- }
-
- private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags) {
- mSourceIntents.addAll(other.getAllSourceIntents());
- mResolveInfo = other.mResolveInfo;
- mDisplayLabel = other.mDisplayLabel;
- mDisplayIcon = other.mDisplayIcon;
- mExtendedInfo = other.mExtendedInfo;
- mResolvedIntent = new Intent(other.mResolvedIntent);
- mResolvedIntent.fillIn(fillInIntent, flags);
- }
-
- public ResolveInfo getResolveInfo() {
- return mResolveInfo;
- }
-
- public CharSequence getDisplayLabel() {
- if (mDisplayLabel == null) {
- ResolveInfoPresentationGetter pg = makePresentationGetter(mResolveInfo);
- mDisplayLabel = pg.getLabel();
- mExtendedInfo = pg.getSubLabel();
- }
- return mDisplayLabel;
- }
-
- public boolean hasDisplayLabel() {
- return mDisplayLabel != null;
- }
-
- public void setDisplayLabel(CharSequence displayLabel) {
- mDisplayLabel = displayLabel;
- }
-
- public void setExtendedInfo(CharSequence extendedInfo) {
- mExtendedInfo = extendedInfo;
- }
-
- public Drawable getDisplayIcon() {
- return mDisplayIcon;
- }
-
- @Override
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return new DisplayResolveInfo(this, fillInIntent, flags);
- }
-
- @Override
- public List<Intent> getAllSourceIntents() {
- return mSourceIntents;
- }
-
- public void addAlternateSourceIntent(Intent alt) {
- mSourceIntents.add(alt);
- }
-
- public void setDisplayIcon(Drawable icon) {
- mDisplayIcon = icon;
- }
-
- public boolean hasDisplayIcon() {
- return mDisplayIcon != null;
- }
-
- public CharSequence getExtendedInfo() {
- return mExtendedInfo;
- }
-
- public Intent getResolvedIntent() {
- return mResolvedIntent;
- }
-
- @Override
- public ComponentName getResolvedComponentName() {
- return new ComponentName(mResolveInfo.activityInfo.packageName,
- mResolveInfo.activityInfo.name);
- }
-
- @Override
- public boolean start(Activity activity, Bundle options) {
- activity.startActivity(mResolvedIntent, options);
- return true;
- }
-
- @Override
- public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
- if (mEnableChooserDelegate) {
- return activity.startAsCallerImpl(mResolvedIntent, options, false, userId);
- } else {
- activity.startActivityAsCaller(mResolvedIntent, options, null, false, userId);
- return true;
- }
- }
-
- @Override
- public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
- activity.startActivityAsUser(mResolvedIntent, options, user);
- return false;
- }
-
- public boolean isSuspended() {
- return mIsSuspended;
- }
- }
-
- List<DisplayResolveInfo> getDisplayList() {
- return mAdapter.mDisplayList;
- }
-
- /**
- * A single target as represented in the chooser.
- */
- public interface TargetInfo {
- /**
- * Get the resolved intent that represents this target. Note that this may not be the
- * intent that will be launched by calling one of the <code>start</code> methods provided;
- * this is the intent that will be credited with the launch.
- *
- * @return the resolved intent for this target
- */
- Intent getResolvedIntent();
-
- /**
- * Get the resolved component name that represents this target. Note that this may not
- * be the component that will be directly launched by calling one of the <code>start</code>
- * methods provided; this is the component that will be credited with the launch.
- *
- * @return the resolved ComponentName for this target
- */
- ComponentName getResolvedComponentName();
-
- /**
- * Start the activity referenced by this target.
- *
- * @param activity calling Activity performing the launch
- * @param options ActivityOptions bundle
- * @return true if the start completed successfully
- */
- boolean start(Activity activity, Bundle options);
-
- /**
- * Start the activity referenced by this target as if the ResolverActivity's caller
- * was performing the start operation.
- *
- * @param activity calling Activity (actually) performing the launch
- * @param options ActivityOptions bundle
- * @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller
- * @return true if the start completed successfully
- */
- boolean startAsCaller(ResolverActivity activity, Bundle options, int userId);
-
- /**
- * Start the activity referenced by this target as a given user.
- *
- * @param activity calling activity performing the launch
- * @param options ActivityOptions bundle
- * @param user handle for the user to start the activity as
- * @return true if the start completed successfully
- */
- boolean startAsUser(Activity activity, Bundle options, UserHandle user);
-
- /**
- * Return the ResolveInfo about how and why this target matched the original query
- * for available targets.
- *
- * @return ResolveInfo representing this target's match
- */
- ResolveInfo getResolveInfo();
-
- /**
- * Return the human-readable text label for this target.
- *
- * @return user-visible target label
- */
- CharSequence getDisplayLabel();
-
- /**
- * Return any extended info for this target. This may be used to disambiguate
- * otherwise identical targets.
- *
- * @return human-readable disambig string or null if none present
- */
- CharSequence getExtendedInfo();
-
- /**
- * @return The drawable that should be used to represent this target including badge
- */
- Drawable getDisplayIcon();
-
- /**
- * Clone this target with the given fill-in information.
- */
- TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
-
- /**
- * @return the list of supported source intents deduped against this single target
- */
- List<Intent> getAllSourceIntents();
-
- /**
- * @return true if this target can be selected by the user
- */
- boolean isSuspended();
- }
-
- public class ResolveListAdapter extends BaseAdapter {
- private final List<Intent> mIntents;
- private final Intent[] mInitialIntents;
- private final List<ResolveInfo> mBaseResolveList;
- protected ResolveInfo mLastChosen;
- private DisplayResolveInfo mOtherProfile;
- ResolverListController mResolverListController;
- private int mPlaceholderCount;
- private boolean mAllTargetsAreBrowsers = false;
-
- protected final LayoutInflater mInflater;
-
- // This one is the list that the Adapter will actually present.
- List<DisplayResolveInfo> mDisplayList;
- List<ResolvedComponentInfo> mUnfilteredResolveList;
-
- private int mLastChosenPosition = -1;
- private boolean mFilterLastUsed;
-
- public ResolveListAdapter(Context context, List<Intent> payloadIntents,
- Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
- boolean filterLastUsed,
- ResolverListController resolverListController) {
- mIntents = payloadIntents;
- mInitialIntents = initialIntents;
- mBaseResolveList = rList;
- mLaunchedFromUid = launchedFromUid;
- mInflater = LayoutInflater.from(context);
- mDisplayList = new ArrayList<>();
- mFilterLastUsed = filterLastUsed;
- mResolverListController = resolverListController;
- }
-
- public void handlePackagesChanged() {
- rebuildList();
- if (getCount() == 0) {
- // We no longer have any items... just finish the activity.
- finish();
- }
- }
-
- public void setPlaceholderCount(int count) {
- mPlaceholderCount = count;
- }
-
- public int getPlaceholderCount() { return mPlaceholderCount; }
-
- @Nullable
- public DisplayResolveInfo getFilteredItem() {
- if (mFilterLastUsed && mLastChosenPosition >= 0) {
- // Not using getItem since it offsets to dodge this position for the list
- return mDisplayList.get(mLastChosenPosition);
- }
- return null;
- }
-
- public DisplayResolveInfo getOtherProfile() {
- return mOtherProfile;
- }
-
- public int getFilteredPosition() {
- if (mFilterLastUsed && mLastChosenPosition >= 0) {
- return mLastChosenPosition;
- }
- return AbsListView.INVALID_POSITION;
- }
-
- public boolean hasFilteredItem() {
- return mFilterLastUsed && mLastChosen != null;
- }
-
- public float getScore(DisplayResolveInfo target) {
- return mResolverListController.getScore(target);
- }
-
- public void updateModel(ComponentName componentName) {
- mResolverListController.updateModel(componentName);
- }
-
- public void updateChooserCounts(String packageName, int userId, String action) {
- mResolverListController.updateChooserCounts(packageName, userId, action);
- }
-
- /**
- * @return true if all items in the display list are defined as browsers by
- * ResolveInfo.handleAllWebDataURI
- */
- public boolean areAllTargetsBrowsers() {
- return mAllTargetsAreBrowsers;
- }
-
- /**
- * Rebuild the list of resolvers. In some cases some parts will need some asynchronous work
- * to complete.
- *
- * @return Whether or not the list building is completed.
- */
- protected boolean rebuildList() {
- List<ResolvedComponentInfo> currentResolveList = null;
- // Clear the value of mOtherProfile from previous call.
- mOtherProfile = null;
- mLastChosen = null;
- mLastChosenPosition = -1;
- mAllTargetsAreBrowsers = false;
- mDisplayList.clear();
- if (mBaseResolveList != null) {
- currentResolveList = mUnfilteredResolveList = new ArrayList<>();
- mResolverListController.addResolveListDedupe(currentResolveList,
- getTargetIntent(),
- mBaseResolveList);
- } else {
- currentResolveList = mUnfilteredResolveList =
- mResolverListController.getResolversForIntent(shouldGetResolvedFilter(),
- shouldGetActivityMetadata(),
- mIntents);
- if (currentResolveList == null) {
- processSortedList(currentResolveList);
- return true;
- }
- List<ResolvedComponentInfo> originalList =
- mResolverListController.filterIneligibleActivities(currentResolveList,
- true);
- if (originalList != null) {
- mUnfilteredResolveList = originalList;
- }
- }
-
- // So far we only support a single other profile at a time.
- // The first one we see gets special treatment.
- for (ResolvedComponentInfo info : currentResolveList) {
- if (info.getResolveInfoAt(0).targetUserId != UserHandle.USER_CURRENT) {
- mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0),
- info.getResolveInfoAt(0),
- info.getResolveInfoAt(0).loadLabel(mPm),
- info.getResolveInfoAt(0).loadLabel(mPm),
- getReplacementIntent(info.getResolveInfoAt(0).activityInfo,
- info.getIntentAt(0)));
- currentResolveList.remove(info);
- break;
- }
- }
-
- if (mOtherProfile == null) {
- try {
- mLastChosen = mResolverListController.getLastChosen();
- } catch (RemoteException re) {
- Log.d(TAG, "Error calling getLastChosenActivity\n" + re);
- }
- }
-
- int N;
- if ((currentResolveList != null) && ((N = currentResolveList.size()) > 0)) {
- // We only care about fixing the unfilteredList if the current resolve list and
- // current resolve list are currently the same.
- List<ResolvedComponentInfo> originalList =
- mResolverListController.filterLowPriority(currentResolveList,
- mUnfilteredResolveList == currentResolveList);
- if (originalList != null) {
- mUnfilteredResolveList = originalList;
- }
-
- if (currentResolveList.size() > 1) {
- int placeholderCount = currentResolveList.size();
- if (useLayoutWithDefault()) {
- --placeholderCount;
- }
- setPlaceholderCount(placeholderCount);
- createSortingTask().execute(currentResolveList);
- postListReadyRunnable();
- return false;
- } else {
- processSortedList(currentResolveList);
- return true;
- }
- } else {
- processSortedList(currentResolveList);
- return true;
- }
- }
-
- AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>> createSortingTask() {
- return new AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>>() {
- @Override
- protected List<ResolvedComponentInfo> doInBackground(
- List<ResolvedComponentInfo>... params) {
- mResolverListController.sort(params[0]);
- return params[0];
- }
-
- @Override
- protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
- processSortedList(sortedComponents);
- bindProfileView();
- notifyDataSetChanged();
- }
- };
- }
-
- void processSortedList(List<ResolvedComponentInfo> sortedComponents) {
- int N;
- if (sortedComponents != null && (N = sortedComponents.size()) != 0) {
- mAllTargetsAreBrowsers = mUseLayoutForBrowsables;
-
- // First put the initial items at the top.
- if (mInitialIntents != null) {
- for (int i = 0; i < mInitialIntents.length; i++) {
- Intent ii = mInitialIntents[i];
- if (ii == null) {
- continue;
- }
- ActivityInfo ai = ii.resolveActivityInfo(
- getPackageManager(), 0);
- if (ai == null) {
- Log.w(TAG, "No activity found for " + ii);
- continue;
- }
- ResolveInfo ri = new ResolveInfo();
- ri.activityInfo = ai;
- UserManager userManager =
- (UserManager) getSystemService(Context.USER_SERVICE);
- if (ii instanceof LabeledIntent) {
- LabeledIntent li = (LabeledIntent) ii;
- ri.resolvePackageName = li.getSourcePackage();
- ri.labelRes = li.getLabelResource();
- ri.nonLocalizedLabel = li.getNonLocalizedLabel();
- ri.icon = li.getIconResource();
- ri.iconResourceId = ri.icon;
- }
- if (userManager.isManagedProfile()) {
- ri.noResourceId = true;
- ri.icon = 0;
- }
- mAllTargetsAreBrowsers &= ri.handleAllWebDataURI;
-
- addResolveInfo(new DisplayResolveInfo(ii, ri,
- ri.loadLabel(getPackageManager()), null, ii));
- }
- }
-
-
- for (ResolvedComponentInfo rci : sortedComponents) {
- final ResolveInfo ri = rci.getResolveInfoAt(0);
- if (ri != null) {
- mAllTargetsAreBrowsers &= ri.handleAllWebDataURI;
- addResolveInfoWithAlternates(rci);
- }
- }
- }
-
- sendVoiceChoicesIfNeeded();
- postListReadyRunnable();
- }
-
-
-
- /**
- * Some necessary methods for creating the list are initiated in onCreate and will also
- * determine the layout known. We therefore can't update the UI inline and post to the
- * handler thread to update after the current task is finished.
- */
- private void postListReadyRunnable() {
- if (mPostListReadyRunnable == null) {
- mPostListReadyRunnable = new Runnable() {
- @Override
- public void run() {
- setHeader();
- resetButtonBar();
- onListRebuilt();
- mPostListReadyRunnable = null;
- }
- };
- getMainThreadHandler().post(mPostListReadyRunnable);
- }
- }
-
- public void onListRebuilt() {
- int count = getUnfilteredCount();
- if (count == 1 && getOtherProfile() == null) {
- // Only one target, so we're a candidate to auto-launch!
- final TargetInfo target = targetInfoForPosition(0, false);
- if (shouldAutoLaunchSingleChoice(target)) {
- safelyStartActivity(target);
- finish();
- }
- }
- }
-
- public boolean shouldGetResolvedFilter() {
- return mFilterLastUsed;
- }
-
- private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) {
- final int count = rci.getCount();
- final Intent intent = rci.getIntentAt(0);
- final ResolveInfo add = rci.getResolveInfoAt(0);
- final Intent replaceIntent = getReplacementIntent(add.activityInfo, intent);
- final DisplayResolveInfo dri = new DisplayResolveInfo(intent, add, replaceIntent);
- addResolveInfo(dri);
- if (replaceIntent == intent) {
- // Only add alternates if we didn't get a specific replacement from
- // the caller. If we have one it trumps potential alternates.
- for (int i = 1, N = count; i < N; i++) {
- final Intent altIntent = rci.getIntentAt(i);
- dri.addAlternateSourceIntent(altIntent);
- }
- }
- updateLastChosenPosition(add);
- }
-
- private void updateLastChosenPosition(ResolveInfo info) {
- // If another profile is present, ignore the last chosen entry.
- if (mOtherProfile != null) {
- mLastChosenPosition = -1;
- return;
- }
- if (mLastChosen != null
- && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName)
- && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) {
- mLastChosenPosition = mDisplayList.size() - 1;
- }
- }
-
- // We assume that at this point we've already filtered out the only intent for a different
- // targetUserId which we're going to use.
- private void addResolveInfo(DisplayResolveInfo dri) {
- if (dri != null && dri.mResolveInfo != null
- && dri.mResolveInfo.targetUserId == UserHandle.USER_CURRENT) {
- // Checks if this info is already listed in display.
- for (DisplayResolveInfo existingInfo : mDisplayList) {
- if (resolveInfoMatch(dri.mResolveInfo, existingInfo.mResolveInfo)) {
- return;
- }
- }
- mDisplayList.add(dri);
- }
- }
-
- @Nullable
- public ResolveInfo resolveInfoForPosition(int position, boolean filtered) {
- TargetInfo target = targetInfoForPosition(position, filtered);
- if (target != null) {
- return target.getResolveInfo();
- }
- return null;
- }
-
- @Nullable
- public TargetInfo targetInfoForPosition(int position, boolean filtered) {
- if (filtered) {
- return getItem(position);
- }
- if (mDisplayList.size() > position) {
- return mDisplayList.get(position);
- }
- return null;
- }
-
- public int getCount() {
- int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount :
- mDisplayList.size();
- if (mFilterLastUsed && mLastChosenPosition >= 0) {
- totalSize--;
- }
- return totalSize;
- }
-
- public int getUnfilteredCount() {
- return mDisplayList.size();
- }
-
- @Nullable
- public TargetInfo getItem(int position) {
- if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) {
- position++;
- }
- if (mDisplayList.size() > position) {
- return mDisplayList.get(position);
- } else {
- return null;
- }
- }
-
- public long getItemId(int position) {
- return position;
- }
-
- public int getDisplayResolveInfoCount() {
- return mDisplayList.size();
- }
-
- public DisplayResolveInfo getDisplayResolveInfo(int index) {
- // Used to query services. We only query services for primary targets, not alternates.
- return mDisplayList.get(index);
- }
-
- public final View getView(int position, View convertView, ViewGroup parent) {
- View view = convertView;
- if (view == null) {
- view = createView(parent);
- }
- onBindView(view, getItem(position));
- return view;
- }
-
- public final View createView(ViewGroup parent) {
- final View view = onCreateView(parent);
- final ViewHolder holder = new ViewHolder(view);
- view.setTag(holder);
- return view;
- }
-
- public View onCreateView(ViewGroup parent) {
- return mInflater.inflate(
- com.android.internal.R.layout.resolve_list_item, parent, false);
- }
-
- public final void bindView(int position, View view) {
- onBindView(view, getItem(position));
- }
-
- protected void onBindView(View view, TargetInfo info) {
- final ViewHolder holder = (ViewHolder) view.getTag();
- if (info == null) {
- holder.icon.setImageDrawable(
- getDrawable(R.drawable.resolver_icon_placeholder));
- return;
- }
-
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayLabel()) {
- getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
- } else {
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo());
- }
-
- if (info.isSuspended()) {
- holder.icon.setColorFilter(mSuspendedMatrixColorFilter);
- } else {
- holder.icon.setColorFilter(null);
- }
-
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayIcon()) {
- new LoadIconTask((DisplayResolveInfo) info, holder.icon).execute();
- } else {
- holder.icon.setImageDrawable(info.getDisplayIcon());
- }
+ @Override // ResolverListCommunicator
+ public void onHandlePackagesChanged() {
+ if (mAdapter.getCount() == 0) {
+ // We no longer have any items... just finish the activity.
+ finish();
}
}
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelTask(info, holder);
- }
-
@VisibleForTesting
public static final class ResolvedComponentInfo {
public final ComponentName name;
@@ -2144,41 +1280,6 @@ public class ResolverActivity extends Activity {
}
}
- static class ViewHolder {
- public View itemView;
- public Drawable defaultItemViewBackground;
-
- public TextView text;
- public TextView text2;
- public ImageView icon;
-
- public ViewHolder(View view) {
- itemView = view;
- defaultItemViewBackground = view.getBackground();
- text = (TextView) view.findViewById(com.android.internal.R.id.text1);
- text2 = (TextView) view.findViewById(com.android.internal.R.id.text2);
- icon = (ImageView) view.findViewById(R.id.icon);
- }
-
- public void bindLabel(CharSequence label, CharSequence subLabel) {
- if (!TextUtils.equals(text.getText(), label)) {
- text.setText(label);
- }
-
- // Always show a subLabel for visual consistency across list items. Show an empty
- // subLabel if the subLabel is the same as the label
- if (TextUtils.equals(label, subLabel)) {
- subLabel = null;
- }
-
- if (!TextUtils.equals(text2.getText(), subLabel)
- && !TextUtils.isEmpty(subLabel)) {
- text2.setVisibility(View.VISIBLE);
- text2.setText(subLabel);
- }
- }
- }
-
class ItemClickListener implements AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener {
@Override
@@ -2229,61 +1330,6 @@ public class ResolverActivity extends Activity {
}
- protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
- private final DisplayResolveInfo mDisplayResolveInfo;
- private final ViewHolder mHolder;
-
- protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
- mDisplayResolveInfo = dri;
- mHolder = holder;
- }
-
-
- @Override
- protected CharSequence[] doInBackground(Void... voids) {
- ResolveInfoPresentationGetter pg =
- makePresentationGetter(mDisplayResolveInfo.mResolveInfo);
- return new CharSequence[] {
- pg.getLabel(),
- pg.getSubLabel()
- };
- }
-
- @Override
- protected void onPostExecute(CharSequence[] result) {
- mDisplayResolveInfo.setDisplayLabel(result[0]);
- mDisplayResolveInfo.setExtendedInfo(result[1]);
- mHolder.bindLabel(result[0], result[1]);
- }
- }
-
- class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
- protected final DisplayResolveInfo mDisplayResolveInfo;
- private final ResolveInfo mResolveInfo;
- private final ImageView mTargetView;
-
- LoadIconTask(DisplayResolveInfo dri, ImageView target) {
- mDisplayResolveInfo = dri;
- mResolveInfo = dri.getResolveInfo();
- mTargetView = target;
- }
-
- @Override
- protected Drawable doInBackground(Void... params) {
- return loadIconForResolveInfo(mResolveInfo);
- }
-
- @Override
- protected void onPostExecute(Drawable d) {
- if (mAdapter.getOtherProfile() == mDisplayResolveInfo) {
- bindProfileView();
- } else {
- mDisplayResolveInfo.setDisplayIcon(d);
- mTargetView.setImageDrawable(d);
- }
- }
- }
-
static final boolean isSpecificUriMatch(int match) {
match = match&IntentFilter.MATCH_CATEGORY_MASK;
return match >= IntentFilter.MATCH_CATEGORY_HOST
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
new file mode 100644
index 000000000000..4076ddaa71b0
--- /dev/null
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -0,0 +1,862 @@
+/*
+ * 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.internal.app;
+
+import static android.content.Context.ACTIVITY_SERVICE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LabeledIntent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
+import com.android.internal.app.chooser.DisplayResolveInfo;
+import com.android.internal.app.chooser.TargetInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ResolverListAdapter extends BaseAdapter {
+ private static final String TAG = "ResolverListAdapter";
+
+ private final List<Intent> mIntents;
+ private final Intent[] mInitialIntents;
+ private final List<ResolveInfo> mBaseResolveList;
+ private final PackageManager mPm;
+ protected final Context mContext;
+ private final ColorMatrixColorFilter mSuspendedMatrixColorFilter;
+ private final boolean mUseLayoutForBrowsables;
+ private final int mIconDpi;
+ protected ResolveInfo mLastChosen;
+ private DisplayResolveInfo mOtherProfile;
+ ResolverListController mResolverListController;
+ private int mPlaceholderCount;
+ private boolean mAllTargetsAreBrowsers = false;
+
+ protected final LayoutInflater mInflater;
+
+ // This one is the list that the Adapter will actually present.
+ List<DisplayResolveInfo> mDisplayList;
+ List<ResolvedComponentInfo> mUnfilteredResolveList;
+
+ private int mLastChosenPosition = -1;
+ private boolean mFilterLastUsed;
+ private final ResolverListCommunicator mResolverListCommunicator;
+ private Runnable mPostListReadyRunnable;
+
+ public ResolverListAdapter(Context context, List<Intent> payloadIntents,
+ Intent[] initialIntents, List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ boolean useLayoutForBrowsables,
+ ResolverListCommunicator resolverListCommunicator) {
+ mContext = context;
+ mIntents = payloadIntents;
+ mInitialIntents = initialIntents;
+ mBaseResolveList = rList;
+ mInflater = LayoutInflater.from(context);
+ mPm = context.getPackageManager();
+ mDisplayList = new ArrayList<>();
+ mFilterLastUsed = filterLastUsed;
+ mResolverListController = resolverListController;
+ mSuspendedMatrixColorFilter = createSuspendedColorMatrix();
+ mUseLayoutForBrowsables = useLayoutForBrowsables;
+ mResolverListCommunicator = resolverListCommunicator;
+ final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE);
+ mIconDpi = am.getLauncherLargeIconDensity();
+ }
+
+ public void handlePackagesChanged() {
+ rebuildList();
+ mResolverListCommunicator.onHandlePackagesChanged();
+ }
+
+ public void setPlaceholderCount(int count) {
+ mPlaceholderCount = count;
+ }
+
+ public int getPlaceholderCount() {
+ return mPlaceholderCount;
+ }
+
+ @Nullable
+ public DisplayResolveInfo getFilteredItem() {
+ if (mFilterLastUsed && mLastChosenPosition >= 0) {
+ // Not using getItem since it offsets to dodge this position for the list
+ return mDisplayList.get(mLastChosenPosition);
+ }
+ return null;
+ }
+
+ public DisplayResolveInfo getOtherProfile() {
+ return mOtherProfile;
+ }
+
+ public int getFilteredPosition() {
+ if (mFilterLastUsed && mLastChosenPosition >= 0) {
+ return mLastChosenPosition;
+ }
+ return AbsListView.INVALID_POSITION;
+ }
+
+ public boolean hasFilteredItem() {
+ return mFilterLastUsed && mLastChosen != null;
+ }
+
+ public float getScore(DisplayResolveInfo target) {
+ return mResolverListController.getScore(target);
+ }
+
+ public void updateModel(ComponentName componentName) {
+ mResolverListController.updateModel(componentName);
+ }
+
+ public void updateChooserCounts(String packageName, int userId, String action) {
+ mResolverListController.updateChooserCounts(packageName, userId, action);
+ }
+
+ /**
+ * @return true if all items in the display list are defined as browsers by
+ * ResolveInfo.handleAllWebDataURI
+ */
+ public boolean areAllTargetsBrowsers() {
+ return mAllTargetsAreBrowsers;
+ }
+
+ /**
+ * Rebuild the list of resolvers. In some cases some parts will need some asynchronous work
+ * to complete.
+ *
+ * @return Whether or not the list building is completed.
+ */
+ protected boolean rebuildList() {
+ List<ResolvedComponentInfo> currentResolveList = null;
+ // Clear the value of mOtherProfile from previous call.
+ mOtherProfile = null;
+ mLastChosen = null;
+ mLastChosenPosition = -1;
+ mAllTargetsAreBrowsers = false;
+ mDisplayList.clear();
+ if (mBaseResolveList != null) {
+ currentResolveList = mUnfilteredResolveList = new ArrayList<>();
+ mResolverListController.addResolveListDedupe(currentResolveList,
+ mResolverListCommunicator.getTargetIntent(),
+ mBaseResolveList);
+ } else {
+ currentResolveList = mUnfilteredResolveList =
+ mResolverListController.getResolversForIntent(shouldGetResolvedFilter(),
+ mResolverListCommunicator.shouldGetActivityMetadata(),
+ mIntents);
+ if (currentResolveList == null) {
+ processSortedList(currentResolveList);
+ return true;
+ }
+ List<ResolvedComponentInfo> originalList =
+ mResolverListController.filterIneligibleActivities(currentResolveList,
+ true);
+ if (originalList != null) {
+ mUnfilteredResolveList = originalList;
+ }
+ }
+
+ // So far we only support a single other profile at a time.
+ // The first one we see gets special treatment.
+ for (ResolvedComponentInfo info : currentResolveList) {
+ ResolveInfo resolveInfo = info.getResolveInfoAt(0);
+ if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
+ Intent pOrigIntent = mResolverListCommunicator.getReplacementIntent(
+ resolveInfo.activityInfo,
+ info.getIntentAt(0));
+ Intent replacementIntent = mResolverListCommunicator.getReplacementIntent(
+ resolveInfo.activityInfo,
+ mResolverListCommunicator.getTargetIntent());
+ mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0),
+ resolveInfo,
+ resolveInfo.loadLabel(mPm),
+ resolveInfo.loadLabel(mPm),
+ pOrigIntent != null ? pOrigIntent : replacementIntent,
+ makePresentationGetter(resolveInfo));
+ currentResolveList.remove(info);
+ break;
+ }
+ }
+
+ if (mOtherProfile == null) {
+ try {
+ mLastChosen = mResolverListController.getLastChosen();
+ } catch (RemoteException re) {
+ Log.d(TAG, "Error calling getLastChosenActivity\n" + re);
+ }
+ }
+
+ int n;
+ if ((currentResolveList != null) && ((n = currentResolveList.size()) > 0)) {
+ // We only care about fixing the unfilteredList if the current resolve list and
+ // current resolve list are currently the same.
+ List<ResolvedComponentInfo> originalList =
+ mResolverListController.filterLowPriority(currentResolveList,
+ mUnfilteredResolveList == currentResolveList);
+ if (originalList != null) {
+ mUnfilteredResolveList = originalList;
+ }
+
+ if (currentResolveList.size() > 1) {
+ int placeholderCount = currentResolveList.size();
+ if (mResolverListCommunicator.useLayoutWithDefault()) {
+ --placeholderCount;
+ }
+ setPlaceholderCount(placeholderCount);
+ createSortingTask().execute(currentResolveList);
+ postListReadyRunnable();
+ return false;
+ } else {
+ processSortedList(currentResolveList);
+ return true;
+ }
+ } else {
+ processSortedList(currentResolveList);
+ return true;
+ }
+ }
+
+ AsyncTask<List<ResolvedComponentInfo>,
+ Void,
+ List<ResolvedComponentInfo>> createSortingTask() {
+ return new AsyncTask<List<ResolvedComponentInfo>,
+ Void,
+ List<ResolvedComponentInfo>>() {
+ @Override
+ protected List<ResolvedComponentInfo> doInBackground(
+ List<ResolvedComponentInfo>... params) {
+ mResolverListController.sort(params[0]);
+ return params[0];
+ }
+ @Override
+ protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
+ processSortedList(sortedComponents);
+ mResolverListCommunicator.updateProfileViewButton();
+ notifyDataSetChanged();
+ }
+ };
+ }
+
+
+ protected void processSortedList(List<ResolvedComponentInfo> sortedComponents) {
+ int n;
+ if (sortedComponents != null && (n = sortedComponents.size()) != 0) {
+ mAllTargetsAreBrowsers = mUseLayoutForBrowsables;
+
+ // First put the initial items at the top.
+ if (mInitialIntents != null) {
+ for (int i = 0; i < mInitialIntents.length; i++) {
+ Intent ii = mInitialIntents[i];
+ if (ii == null) {
+ continue;
+ }
+ ActivityInfo ai = ii.resolveActivityInfo(
+ mPm, 0);
+ if (ai == null) {
+ Log.w(TAG, "No activity found for " + ii);
+ continue;
+ }
+ ResolveInfo ri = new ResolveInfo();
+ ri.activityInfo = ai;
+ UserManager userManager =
+ (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ if (ii instanceof LabeledIntent) {
+ LabeledIntent li = (LabeledIntent) ii;
+ ri.resolvePackageName = li.getSourcePackage();
+ ri.labelRes = li.getLabelResource();
+ ri.nonLocalizedLabel = li.getNonLocalizedLabel();
+ ri.icon = li.getIconResource();
+ ri.iconResourceId = ri.icon;
+ }
+ if (userManager.isManagedProfile()) {
+ ri.noResourceId = true;
+ ri.icon = 0;
+ }
+ mAllTargetsAreBrowsers &= ri.handleAllWebDataURI;
+
+ addResolveInfo(new DisplayResolveInfo(ii, ri,
+ ri.loadLabel(mPm), null, ii, makePresentationGetter(ri)));
+ }
+ }
+
+
+ for (ResolvedComponentInfo rci : sortedComponents) {
+ final ResolveInfo ri = rci.getResolveInfoAt(0);
+ if (ri != null) {
+ mAllTargetsAreBrowsers &= ri.handleAllWebDataURI;
+ addResolveInfoWithAlternates(rci);
+ }
+ }
+ }
+
+ mResolverListCommunicator.sendVoiceChoicesIfNeeded();
+ postListReadyRunnable();
+ }
+
+ /**
+ * Some necessary methods for creating the list are initiated in onCreate and will also
+ * determine the layout known. We therefore can't update the UI inline and post to the
+ * handler thread to update after the current task is finished.
+ */
+ private void postListReadyRunnable() {
+ if (mPostListReadyRunnable == null) {
+ mPostListReadyRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mResolverListCommunicator.onPostListReady();
+ mPostListReadyRunnable = null;
+ }
+ };
+ mContext.getMainThreadHandler().post(mPostListReadyRunnable);
+ }
+ }
+
+ public boolean shouldGetResolvedFilter() {
+ return mFilterLastUsed;
+ }
+
+ private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) {
+ final int count = rci.getCount();
+ final Intent intent = rci.getIntentAt(0);
+ final ResolveInfo add = rci.getResolveInfoAt(0);
+ final Intent replaceIntent =
+ mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent);
+ final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent(
+ add.activityInfo, mResolverListCommunicator.getTargetIntent());
+ final DisplayResolveInfo
+ dri = new DisplayResolveInfo(intent, add,
+ replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add));
+ addResolveInfo(dri);
+ if (replaceIntent == intent) {
+ // Only add alternates if we didn't get a specific replacement from
+ // the caller. If we have one it trumps potential alternates.
+ for (int i = 1, n = count; i < n; i++) {
+ final Intent altIntent = rci.getIntentAt(i);
+ dri.addAlternateSourceIntent(altIntent);
+ }
+ }
+ updateLastChosenPosition(add);
+ }
+
+ private void updateLastChosenPosition(ResolveInfo info) {
+ // If another profile is present, ignore the last chosen entry.
+ if (mOtherProfile != null) {
+ mLastChosenPosition = -1;
+ return;
+ }
+ if (mLastChosen != null
+ && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName)
+ && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) {
+ mLastChosenPosition = mDisplayList.size() - 1;
+ }
+ }
+
+ // We assume that at this point we've already filtered out the only intent for a different
+ // targetUserId which we're going to use.
+ private void addResolveInfo(DisplayResolveInfo dri) {
+ if (dri != null && dri.getResolveInfo() != null
+ && dri.getResolveInfo().targetUserId == UserHandle.USER_CURRENT) {
+ // Checks if this info is already listed in display.
+ for (DisplayResolveInfo existingInfo : mDisplayList) {
+ if (mResolverListCommunicator
+ .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
+ return;
+ }
+ }
+ mDisplayList.add(dri);
+ }
+ }
+
+ @Nullable
+ public ResolveInfo resolveInfoForPosition(int position, boolean filtered) {
+ TargetInfo target = targetInfoForPosition(position, filtered);
+ if (target != null) {
+ return target.getResolveInfo();
+ }
+ return null;
+ }
+
+ @Nullable
+ public TargetInfo targetInfoForPosition(int position, boolean filtered) {
+ if (filtered) {
+ return getItem(position);
+ }
+ if (mDisplayList.size() > position) {
+ return mDisplayList.get(position);
+ }
+ return null;
+ }
+
+ public int getCount() {
+ int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount :
+ mDisplayList.size();
+ if (mFilterLastUsed && mLastChosenPosition >= 0) {
+ totalSize--;
+ }
+ return totalSize;
+ }
+
+ public int getUnfilteredCount() {
+ return mDisplayList.size();
+ }
+
+ @Nullable
+ public TargetInfo getItem(int position) {
+ if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) {
+ position++;
+ }
+ if (mDisplayList.size() > position) {
+ return mDisplayList.get(position);
+ } else {
+ return null;
+ }
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public int getDisplayResolveInfoCount() {
+ return mDisplayList.size();
+ }
+
+ public DisplayResolveInfo getDisplayResolveInfo(int index) {
+ // Used to query services. We only query services for primary targets, not alternates.
+ return mDisplayList.get(index);
+ }
+
+ public final View getView(int position, View convertView, ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view = createView(parent);
+ }
+ onBindView(view, getItem(position));
+ return view;
+ }
+
+ public final View createView(ViewGroup parent) {
+ final View view = onCreateView(parent);
+ final ViewHolder holder = new ViewHolder(view);
+ view.setTag(holder);
+ return view;
+ }
+
+ public View onCreateView(ViewGroup parent) {
+ return mInflater.inflate(
+ com.android.internal.R.layout.resolve_list_item, parent, false);
+ }
+
+ public final void bindView(int position, View view) {
+ onBindView(view, getItem(position));
+ }
+
+ protected void onBindView(View view, TargetInfo info) {
+ final ViewHolder holder = (ViewHolder) view.getTag();
+ if (info == null) {
+ holder.icon.setImageDrawable(
+ mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+ return;
+ }
+
+ if (info instanceof DisplayResolveInfo
+ && !((DisplayResolveInfo) info).hasDisplayLabel()) {
+ getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
+ } else {
+ holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo());
+ }
+
+ if (info.isSuspended()) {
+ holder.icon.setColorFilter(mSuspendedMatrixColorFilter);
+ } else {
+ holder.icon.setColorFilter(null);
+ }
+
+ if (info instanceof DisplayResolveInfo
+ && !((DisplayResolveInfo) info).hasDisplayIcon()) {
+ new ResolverListAdapter.LoadIconTask((DisplayResolveInfo) info, holder.icon).execute();
+ } else {
+ holder.icon.setImageDrawable(info.getDisplayIcon(mContext));
+ }
+ }
+
+ protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
+ return new LoadLabelTask(info, holder);
+ }
+
+ public void onDestroy() {
+ if (mPostListReadyRunnable != null) {
+ mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
+ mPostListReadyRunnable = null;
+ }
+ if (mResolverListController != null) {
+ mResolverListController.destroy();
+ }
+ }
+
+ private ColorMatrixColorFilter createSuspendedColorMatrix() {
+ int grayValue = 127;
+ float scale = 0.5f; // half bright
+
+ ColorMatrix tempBrightnessMatrix = new ColorMatrix();
+ float[] mat = tempBrightnessMatrix.getArray();
+ mat[0] = scale;
+ mat[6] = scale;
+ mat[12] = scale;
+ mat[4] = grayValue;
+ mat[9] = grayValue;
+ mat[14] = grayValue;
+
+ ColorMatrix matrix = new ColorMatrix();
+ matrix.setSaturation(0.0f);
+ matrix.preConcat(tempBrightnessMatrix);
+ return new ColorMatrixColorFilter(matrix);
+ }
+
+ ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) {
+ return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai);
+ }
+
+ ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) {
+ return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri);
+ }
+
+ Drawable loadIconForResolveInfo(ResolveInfo ri) {
+ // Load icons based on the current process. If in work profile icons should be badged.
+ return makePresentationGetter(ri).getIcon(Process.myUserHandle());
+ }
+
+ void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
+ final DisplayResolveInfo iconInfo = getFilteredItem();
+ if (iconView != null && iconInfo != null) {
+ new LoadIconTask(iconInfo, iconView).execute();
+ }
+ }
+
+ /**
+ * Necessary methods to communicate between {@link ResolverListAdapter}
+ * and {@link ResolverActivity}.
+ */
+ interface ResolverListCommunicator {
+
+ boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs);
+
+ Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
+
+ void onPostListReady();
+
+ void sendVoiceChoicesIfNeeded();
+
+ void updateProfileViewButton();
+
+ boolean useLayoutWithDefault();
+
+ boolean shouldGetActivityMetadata();
+
+ Intent getTargetIntent();
+
+ void onHandlePackagesChanged();
+ }
+
+ static class ViewHolder {
+ public View itemView;
+ public Drawable defaultItemViewBackground;
+
+ public TextView text;
+ public TextView text2;
+ public ImageView icon;
+
+ ViewHolder(View view) {
+ itemView = view;
+ defaultItemViewBackground = view.getBackground();
+ text = (TextView) view.findViewById(com.android.internal.R.id.text1);
+ text2 = (TextView) view.findViewById(com.android.internal.R.id.text2);
+ icon = (ImageView) view.findViewById(R.id.icon);
+ }
+
+ public void bindLabel(CharSequence label, CharSequence subLabel) {
+ if (!TextUtils.equals(text.getText(), label)) {
+ text.setText(label);
+ }
+
+ // Always show a subLabel for visual consistency across list items. Show an empty
+ // subLabel if the subLabel is the same as the label
+ if (TextUtils.equals(label, subLabel)) {
+ subLabel = null;
+ }
+
+ if (!TextUtils.equals(text2.getText(), subLabel)
+ && !TextUtils.isEmpty(subLabel)) {
+ text2.setVisibility(View.VISIBLE);
+ text2.setText(subLabel);
+ }
+ }
+ }
+
+ protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
+ private final DisplayResolveInfo mDisplayResolveInfo;
+ private final ViewHolder mHolder;
+
+ protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
+ mDisplayResolveInfo = dri;
+ mHolder = holder;
+ }
+
+ @Override
+ protected CharSequence[] doInBackground(Void... voids) {
+ ResolveInfoPresentationGetter pg =
+ makePresentationGetter(mDisplayResolveInfo.getResolveInfo());
+ return new CharSequence[] {
+ pg.getLabel(),
+ pg.getSubLabel()
+ };
+ }
+
+ @Override
+ protected void onPostExecute(CharSequence[] result) {
+ mDisplayResolveInfo.setDisplayLabel(result[0]);
+ mDisplayResolveInfo.setExtendedInfo(result[1]);
+ mHolder.bindLabel(result[0], result[1]);
+ }
+ }
+
+ class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
+ protected final com.android.internal.app.chooser.DisplayResolveInfo mDisplayResolveInfo;
+ private final ResolveInfo mResolveInfo;
+ private final ImageView mTargetView;
+
+ LoadIconTask(DisplayResolveInfo dri, ImageView target) {
+ mDisplayResolveInfo = dri;
+ mResolveInfo = dri.getResolveInfo();
+ mTargetView = target;
+ }
+
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ return loadIconForResolveInfo(mResolveInfo);
+ }
+
+ @Override
+ protected void onPostExecute(Drawable d) {
+ if (getOtherProfile() == mDisplayResolveInfo) {
+ mResolverListCommunicator.updateProfileViewButton();
+ } else {
+ mDisplayResolveInfo.setDisplayIcon(d);
+ mTargetView.setImageDrawable(d);
+ }
+ }
+ }
+
+ /**
+ * Loads the icon and label for the provided ResolveInfo.
+ */
+ @VisibleForTesting
+ public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter {
+ private final ResolveInfo mRi;
+ public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) {
+ super(ctx, iconDpi, ri.activityInfo);
+ mRi = ri;
+ }
+
+ @Override
+ Drawable getIconSubstituteInternal() {
+ Drawable dr = null;
+ try {
+ // Do not use ResolveInfo#getIconResource() as it defaults to the app
+ if (mRi.resolvePackageName != null && mRi.icon != 0) {
+ dr = loadIconFromResource(
+ mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+ + "couldn't find resources for package", e);
+ }
+
+ // Fall back to ActivityInfo if no icon is found via ResolveInfo
+ if (dr == null) dr = super.getIconSubstituteInternal();
+
+ return dr;
+ }
+
+ @Override
+ String getAppSubLabelInternal() {
+ // Will default to app name if no intent filter or activity label set, make sure to
+ // check if subLabel matches label before final display
+ return (String) mRi.loadLabel(mPm);
+ }
+ }
+
+ /**
+ * Loads the icon and label for the provided ActivityInfo.
+ */
+ @VisibleForTesting
+ public static class ActivityInfoPresentationGetter extends
+ TargetPresentationGetter {
+ private final ActivityInfo mActivityInfo;
+ public ActivityInfoPresentationGetter(Context ctx, int iconDpi,
+ ActivityInfo activityInfo) {
+ super(ctx, iconDpi, activityInfo.applicationInfo);
+ mActivityInfo = activityInfo;
+ }
+
+ @Override
+ Drawable getIconSubstituteInternal() {
+ Drawable dr = null;
+ try {
+ // Do not use ActivityInfo#getIconResource() as it defaults to the app
+ if (mActivityInfo.icon != 0) {
+ dr = loadIconFromResource(
+ mPm.getResourcesForApplication(mActivityInfo.applicationInfo),
+ mActivityInfo.icon);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+ + "couldn't find resources for package", e);
+ }
+
+ return dr;
+ }
+
+ @Override
+ String getAppSubLabelInternal() {
+ // Will default to app name if no activity label set, make sure to check if subLabel
+ // matches label before final display
+ return (String) mActivityInfo.loadLabel(mPm);
+ }
+ }
+
+ /**
+ * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application
+ * icon and label over any IntentFilter or Activity icon to increase user understanding, with an
+ * exception for applications that hold the right permission. Always attempts to use available
+ * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
+ * Strings to strip creative formatting.
+ */
+ private abstract static class TargetPresentationGetter {
+ @Nullable abstract Drawable getIconSubstituteInternal();
+ @Nullable abstract String getAppSubLabelInternal();
+
+ private Context mCtx;
+ private final int mIconDpi;
+ private final boolean mHasSubstitutePermission;
+ private final ApplicationInfo mAi;
+
+ protected PackageManager mPm;
+
+ TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) {
+ mCtx = ctx;
+ mPm = ctx.getPackageManager();
+ mAi = ai;
+ mIconDpi = iconDpi;
+ mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission(
+ android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON,
+ mAi.packageName);
+ }
+
+ public Drawable getIcon(UserHandle userHandle) {
+ return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle));
+ }
+
+ public Bitmap getIconBitmap(UserHandle userHandle) {
+ Drawable dr = null;
+ if (mHasSubstitutePermission) {
+ dr = getIconSubstituteInternal();
+ }
+
+ if (dr == null) {
+ try {
+ if (mAi.icon != 0) {
+ dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon);
+ }
+ } catch (PackageManager.NameNotFoundException ignore) {
+ }
+ }
+
+ // Fall back to ApplicationInfo#loadIcon if nothing has been loaded
+ if (dr == null) {
+ dr = mAi.loadIcon(mPm);
+ }
+
+ SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx);
+ Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle);
+ sif.recycle();
+
+ return icon;
+ }
+
+ public String getLabel() {
+ String label = null;
+ // Apps with the substitute permission will always show the sublabel as their label
+ if (mHasSubstitutePermission) {
+ label = getAppSubLabelInternal();
+ }
+
+ if (label == null) {
+ label = (String) mAi.loadLabel(mPm);
+ }
+
+ return label;
+ }
+
+ public String getSubLabel() {
+ // Apps with the substitute permission will never have a sublabel
+ if (mHasSubstitutePermission) return null;
+ return getAppSubLabelInternal();
+ }
+
+ protected String loadLabelFromResource(Resources res, int resId) {
+ return res.getString(resId);
+ }
+
+ @Nullable
+ protected Drawable loadIconFromResource(Resources res, int resId) {
+ return res.getDrawableForDensity(resId, mIconDpi);
+ }
+
+ }
+}
diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java
index 28a8a8631372..6cc60b786e55 100644
--- a/core/java/com/android/internal/app/ResolverListController.java
+++ b/core/java/com/android/internal/app/ResolverListController.java
@@ -31,6 +31,7 @@ import android.os.RemoteException;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.chooser.DisplayResolveInfo;
import java.util.ArrayList;
import java.util.Collections;
@@ -332,7 +333,7 @@ public class ResolverListController {
}
@VisibleForTesting
- public float getScore(ResolverActivity.DisplayResolveInfo target) {
+ public float getScore(DisplayResolveInfo target) {
return mResolverComparator.getScore(target.getResolvedComponentName());
}
diff --git a/core/java/com/android/internal/app/SimpleIconFactory.java b/core/java/com/android/internal/app/SimpleIconFactory.java
index 7a4e76fa8c17..d618cdf86865 100644
--- a/core/java/com/android/internal/app/SimpleIconFactory.java
+++ b/core/java/com/android/internal/app/SimpleIconFactory.java
@@ -214,7 +214,7 @@ public class SimpleIconFactory {
* @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
*/
@Deprecated
- Bitmap createAppBadgedIconBitmap(@Nullable Drawable icon, Bitmap renderedAppIcon) {
+ public Bitmap createAppBadgedIconBitmap(@Nullable Drawable icon, Bitmap renderedAppIcon) {
// If no icon is provided use the system default
if (icon == null) {
icon = getFullResDefaultActivityIcon(mFillResIconDpi);
diff --git a/core/java/com/android/internal/app/chooser/ChooserTargetInfo.java b/core/java/com/android/internal/app/chooser/ChooserTargetInfo.java
new file mode 100644
index 000000000000..a2d0953b5620
--- /dev/null
+++ b/core/java/com/android/internal/app/chooser/ChooserTargetInfo.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.internal.app.chooser;
+
+import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+
+/**
+ * A TargetInfo for Direct Share. Includes a {@link ChooserTarget} representing the
+ * Direct Share deep link into an application.
+ */
+public interface ChooserTargetInfo extends TargetInfo {
+ float getModifiedScore();
+
+ ChooserTarget getChooserTarget();
+
+ /**
+ * Do not label as 'equals', since this doesn't quite work
+ * as intended with java 8.
+ */
+ default boolean isSimilar(ChooserTargetInfo other) {
+ if (other == null) return false;
+
+ ChooserTarget ct1 = getChooserTarget();
+ ChooserTarget ct2 = other.getChooserTarget();
+
+ // If either is null, there is not enough info to make an informed decision
+ // about equality, so just exit
+ if (ct1 == null || ct2 == null) return false;
+
+ if (ct1.getComponentName().equals(ct2.getComponentName())
+ && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
+ && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java
new file mode 100644
index 000000000000..c77444e949ed
--- /dev/null
+++ b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java
@@ -0,0 +1,182 @@
+/*
+ * 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.internal.app.chooser;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import com.android.internal.app.ResolverActivity;
+import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A TargetInfo plus additional information needed to render it (such as icon and label) and
+ * resolve it to an activity.
+ */
+public class DisplayResolveInfo implements TargetInfo {
+ // Temporary flag for new chooser delegate behavior.
+ private static final boolean ENABLE_CHOOSER_DELEGATE = true;
+
+ private final ResolveInfo mResolveInfo;
+ private CharSequence mDisplayLabel;
+ private Drawable mDisplayIcon;
+ private CharSequence mExtendedInfo;
+ private final Intent mResolvedIntent;
+ private final List<Intent> mSourceIntents = new ArrayList<>();
+ private boolean mIsSuspended;
+ private ResolveInfoPresentationGetter mResolveInfoPresentationGetter;
+
+ public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent,
+ ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ this(originalIntent, pri, null /*mDisplayLabel*/, null /*mExtendedInfo*/, pOrigIntent,
+ resolveInfoPresentationGetter);
+ }
+
+ public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel,
+ CharSequence pInfo, @NonNull Intent resolvedIntent,
+ @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ mSourceIntents.add(originalIntent);
+ mResolveInfo = pri;
+ mDisplayLabel = pLabel;
+ mExtendedInfo = pInfo;
+ mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+
+ final Intent intent = new Intent(resolvedIntent);
+ intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
+ | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+ final ActivityInfo ai = mResolveInfo.activityInfo;
+ intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
+
+ mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
+
+ mResolvedIntent = intent;
+ }
+
+ private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags,
+ ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ mSourceIntents.addAll(other.getAllSourceIntents());
+ mResolveInfo = other.mResolveInfo;
+ mDisplayLabel = other.mDisplayLabel;
+ mDisplayIcon = other.mDisplayIcon;
+ mExtendedInfo = other.mExtendedInfo;
+ mResolvedIntent = new Intent(other.mResolvedIntent);
+ mResolvedIntent.fillIn(fillInIntent, flags);
+ mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+ }
+
+ public ResolveInfo getResolveInfo() {
+ return mResolveInfo;
+ }
+
+ public CharSequence getDisplayLabel() {
+ if (mDisplayLabel == null && mResolveInfoPresentationGetter != null) {
+ mDisplayLabel = mResolveInfoPresentationGetter.getLabel();
+ mExtendedInfo = mResolveInfoPresentationGetter.getSubLabel();
+ }
+ return mDisplayLabel;
+ }
+
+ public boolean hasDisplayLabel() {
+ return mDisplayLabel != null;
+ }
+
+ public void setDisplayLabel(CharSequence displayLabel) {
+ mDisplayLabel = displayLabel;
+ }
+
+ public void setExtendedInfo(CharSequence extendedInfo) {
+ mExtendedInfo = extendedInfo;
+ }
+
+ public Drawable getDisplayIcon(Context context) {
+ return mDisplayIcon;
+ }
+
+ @Override
+ public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
+ return new DisplayResolveInfo(this, fillInIntent, flags, mResolveInfoPresentationGetter);
+ }
+
+ @Override
+ public List<Intent> getAllSourceIntents() {
+ return mSourceIntents;
+ }
+
+ public void addAlternateSourceIntent(Intent alt) {
+ mSourceIntents.add(alt);
+ }
+
+ public void setDisplayIcon(Drawable icon) {
+ mDisplayIcon = icon;
+ }
+
+ public boolean hasDisplayIcon() {
+ return mDisplayIcon != null;
+ }
+
+ public CharSequence getExtendedInfo() {
+ return mExtendedInfo;
+ }
+
+ public Intent getResolvedIntent() {
+ return mResolvedIntent;
+ }
+
+ @Override
+ public ComponentName getResolvedComponentName() {
+ return new ComponentName(mResolveInfo.activityInfo.packageName,
+ mResolveInfo.activityInfo.name);
+ }
+
+ @Override
+ public boolean start(Activity activity, Bundle options) {
+ activity.startActivity(mResolvedIntent, options);
+ return true;
+ }
+
+ @Override
+ public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ if (ENABLE_CHOOSER_DELEGATE) {
+ return activity.startAsCallerImpl(mResolvedIntent, options, false, userId);
+ } else {
+ activity.startActivityAsCaller(mResolvedIntent, options, null, false, userId);
+ return true;
+ }
+ }
+
+ @Override
+ public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+ activity.startActivityAsUser(mResolvedIntent, options, user);
+ return false;
+ }
+
+ public boolean isSuspended() {
+ return mIsSuspended;
+ }
+}
diff --git a/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java
new file mode 100644
index 000000000000..22cbdaa66267
--- /dev/null
+++ b/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java
@@ -0,0 +1,88 @@
+/*
+ * 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.internal.app.chooser;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.service.chooser.ChooserTarget;
+
+import com.android.internal.app.ResolverActivity;
+
+import java.util.List;
+
+/**
+ * Distinguish between targets that selectable by the user, vs those that are
+ * placeholders for the system while information is loading in an async manner.
+ */
+public abstract class NotSelectableTargetInfo implements ChooserTargetInfo {
+
+ public Intent getResolvedIntent() {
+ return null;
+ }
+
+ public ComponentName getResolvedComponentName() {
+ return null;
+ }
+
+ public boolean start(Activity activity, Bundle options) {
+ return false;
+ }
+
+ public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ return false;
+ }
+
+ public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+ return false;
+ }
+
+ public ResolveInfo getResolveInfo() {
+ return null;
+ }
+
+ public CharSequence getDisplayLabel() {
+ return null;
+ }
+
+ public CharSequence getExtendedInfo() {
+ return null;
+ }
+
+ public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
+ return null;
+ }
+
+ public List<Intent> getAllSourceIntents() {
+ return null;
+ }
+
+ public float getModifiedScore() {
+ return -0.1f;
+ }
+
+ public ChooserTarget getChooserTarget() {
+ return null;
+ }
+
+ public boolean isSuspended() {
+ return false;
+ }
+}
diff --git a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
new file mode 100644
index 000000000000..1cc4857b39fe
--- /dev/null
+++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java
@@ -0,0 +1,316 @@
+/*
+ * 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.internal.app.chooser;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.service.chooser.ChooserTarget;
+import android.text.SpannableStringBuilder;
+import android.util.Log;
+
+import com.android.internal.app.ChooserActivity;
+import com.android.internal.app.ChooserFlags;
+import com.android.internal.app.ResolverActivity;
+import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
+import com.android.internal.app.SimpleIconFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Live target, currently selectable by the user.
+ * @see NotSelectableTargetInfo
+ */
+public final class SelectableTargetInfo implements ChooserTargetInfo {
+ private static final String TAG = "SelectableTargetInfo";
+
+ private final Context mContext;
+ private final DisplayResolveInfo mSourceInfo;
+ private final ResolveInfo mBackupResolveInfo;
+ private final ChooserTarget mChooserTarget;
+ private final String mDisplayLabel;
+ private final PackageManager mPm;
+ private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
+ private Drawable mBadgeIcon = null;
+ private CharSequence mBadgeContentDescription;
+ private Drawable mDisplayIcon;
+ private final Intent mFillInIntent;
+ private final int mFillInFlags;
+ private final float mModifiedScore;
+ private boolean mIsSuspended = false;
+
+ public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo,
+ ChooserTarget chooserTarget,
+ float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator) {
+ mContext = context;
+ mSourceInfo = sourceInfo;
+ mChooserTarget = chooserTarget;
+ mModifiedScore = modifiedScore;
+ mPm = mContext.getPackageManager();
+ mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
+ if (sourceInfo != null) {
+ final ResolveInfo ri = sourceInfo.getResolveInfo();
+ if (ri != null) {
+ final ActivityInfo ai = ri.activityInfo;
+ if (ai != null && ai.applicationInfo != null) {
+ final PackageManager pm = mContext.getPackageManager();
+ mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo);
+ mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo);
+ mIsSuspended =
+ (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
+ }
+ }
+ }
+ // TODO(b/121287224): do this in the background thread, and only for selected targets
+ mDisplayIcon = getChooserTargetIconDrawable(chooserTarget);
+
+ if (sourceInfo != null) {
+ mBackupResolveInfo = null;
+ } else {
+ mBackupResolveInfo =
+ mContext.getPackageManager().resolveActivity(getResolvedIntent(), 0);
+ }
+
+ mFillInIntent = null;
+ mFillInFlags = 0;
+
+ mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle());
+ }
+
+ private SelectableTargetInfo(SelectableTargetInfo other,
+ Intent fillInIntent, int flags) {
+ mContext = other.mContext;
+ mPm = other.mPm;
+ mSelectableTargetInfoCommunicator = other.mSelectableTargetInfoCommunicator;
+ mSourceInfo = other.mSourceInfo;
+ mBackupResolveInfo = other.mBackupResolveInfo;
+ mChooserTarget = other.mChooserTarget;
+ mBadgeIcon = other.mBadgeIcon;
+ mBadgeContentDescription = other.mBadgeContentDescription;
+ mDisplayIcon = other.mDisplayIcon;
+ mFillInIntent = fillInIntent;
+ mFillInFlags = flags;
+ mModifiedScore = other.mModifiedScore;
+
+ mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle());
+ }
+
+ private String sanitizeDisplayLabel(CharSequence label) {
+ SpannableStringBuilder sb = new SpannableStringBuilder(label);
+ sb.clearSpans();
+ return sb.toString();
+ }
+
+ public boolean isSuspended() {
+ return mIsSuspended;
+ }
+
+ /**
+ * Since ShortcutInfos are returned by ShortcutManager, we can cache the shortcuts and skip
+ * the call to LauncherApps#getShortcuts(ShortcutQuery).
+ */
+ // TODO(121287224): Refactor code to apply the suggestion above
+ private Drawable getChooserTargetIconDrawable(ChooserTarget target) {
+ Drawable directShareIcon = null;
+
+ // First get the target drawable and associated activity info
+ final Icon icon = target.getIcon();
+ if (icon != null) {
+ directShareIcon = icon.loadDrawable(mContext);
+ } else if (ChooserFlags.USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS) {
+ Bundle extras = target.getIntentExtras();
+ if (extras != null && extras.containsKey(Intent.EXTRA_SHORTCUT_ID)) {
+ CharSequence shortcutId = extras.getCharSequence(Intent.EXTRA_SHORTCUT_ID);
+ LauncherApps launcherApps = (LauncherApps) mContext.getSystemService(
+ Context.LAUNCHER_APPS_SERVICE);
+ final LauncherApps.ShortcutQuery q = new LauncherApps.ShortcutQuery();
+ q.setPackage(target.getComponentName().getPackageName());
+ q.setShortcutIds(Arrays.asList(shortcutId.toString()));
+ q.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC);
+ final List<ShortcutInfo> shortcuts =
+ launcherApps.getShortcuts(q, mContext.getUser());
+ if (shortcuts != null && shortcuts.size() > 0) {
+ directShareIcon = launcherApps.getShortcutIconDrawable(shortcuts.get(0), 0);
+ }
+ }
+ }
+
+ if (directShareIcon == null) return null;
+
+ ActivityInfo info = null;
+ try {
+ info = mPm.getActivityInfo(target.getComponentName(), 0);
+ } catch (PackageManager.NameNotFoundException error) {
+ Log.e(TAG, "Could not find activity associated with ChooserTarget");
+ }
+
+ if (info == null) return null;
+
+ // Now fetch app icon and raster with no badging even in work profile
+ Bitmap appIcon = mSelectableTargetInfoCommunicator.makePresentationGetter(info)
+ .getIconBitmap(UserHandle.getUserHandleForUid(UserHandle.myUserId()));
+
+ // Raster target drawable with appIcon as a badge
+ SimpleIconFactory sif = SimpleIconFactory.obtain(mContext);
+ Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon);
+ sif.recycle();
+
+ return new BitmapDrawable(mContext.getResources(), directShareBadgedIcon);
+ }
+
+ public float getModifiedScore() {
+ return mModifiedScore;
+ }
+
+ @Override
+ public Intent getResolvedIntent() {
+ if (mSourceInfo != null) {
+ return mSourceInfo.getResolvedIntent();
+ }
+
+ final Intent targetIntent = new Intent(mSelectableTargetInfoCommunicator.getTargetIntent());
+ targetIntent.setComponent(mChooserTarget.getComponentName());
+ targetIntent.putExtras(mChooserTarget.getIntentExtras());
+ return targetIntent;
+ }
+
+ @Override
+ public ComponentName getResolvedComponentName() {
+ if (mSourceInfo != null) {
+ return mSourceInfo.getResolvedComponentName();
+ } else if (mBackupResolveInfo != null) {
+ return new ComponentName(mBackupResolveInfo.activityInfo.packageName,
+ mBackupResolveInfo.activityInfo.name);
+ }
+ return null;
+ }
+
+ private Intent getBaseIntentToSend() {
+ Intent result = getResolvedIntent();
+ if (result == null) {
+ Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
+ } else {
+ result = new Intent(result);
+ if (mFillInIntent != null) {
+ result.fillIn(mFillInIntent, mFillInFlags);
+ }
+ result.fillIn(mSelectableTargetInfoCommunicator.getReferrerFillInIntent(), 0);
+ }
+ return result;
+ }
+
+ @Override
+ public boolean start(Activity activity, Bundle options) {
+ throw new RuntimeException("ChooserTargets should be started as caller.");
+ }
+
+ @Override
+ public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ final Intent intent = getBaseIntentToSend();
+ if (intent == null) {
+ return false;
+ }
+ intent.setComponent(mChooserTarget.getComponentName());
+ intent.putExtras(mChooserTarget.getIntentExtras());
+
+ // Important: we will ignore the target security checks in ActivityManager
+ // if and only if the ChooserTarget's target package is the same package
+ // where we got the ChooserTargetService that provided it. This lets a
+ // ChooserTargetService provide a non-exported or permission-guarded target
+ // to the chooser for the user to pick.
+ //
+ // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
+ // so we'll obey the caller's normal security checks.
+ final boolean ignoreTargetSecurity = mSourceInfo != null
+ && mSourceInfo.getResolvedComponentName().getPackageName()
+ .equals(mChooserTarget.getComponentName().getPackageName());
+ return activity.startAsCallerImpl(intent, options, ignoreTargetSecurity, userId);
+ }
+
+ @Override
+ public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+ throw new RuntimeException("ChooserTargets should be started as caller.");
+ }
+
+ @Override
+ public ResolveInfo getResolveInfo() {
+ return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
+ }
+
+ @Override
+ public CharSequence getDisplayLabel() {
+ return mDisplayLabel;
+ }
+
+ @Override
+ public CharSequence getExtendedInfo() {
+ // ChooserTargets have badge icons, so we won't show the extended info to disambiguate.
+ return null;
+ }
+
+ @Override
+ public Drawable getDisplayIcon(Context context) {
+ return mDisplayIcon;
+ }
+
+ public ChooserTarget getChooserTarget() {
+ return mChooserTarget;
+ }
+
+ @Override
+ public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
+ return new SelectableTargetInfo(this, fillInIntent, flags);
+ }
+
+ @Override
+ public List<Intent> getAllSourceIntents() {
+ final List<Intent> results = new ArrayList<>();
+ if (mSourceInfo != null) {
+ // We only queried the service for the first one in our sourceinfo.
+ results.add(mSourceInfo.getAllSourceIntents().get(0));
+ }
+ return results;
+ }
+
+ /**
+ * Necessary methods to communicate between {@link SelectableTargetInfo}
+ * and {@link ResolverActivity} or {@link ChooserActivity}.
+ */
+ public interface SelectableTargetInfoCommunicator {
+
+ ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info);
+
+ Intent getTargetIntent();
+
+ Intent getReferrerFillInIntent();
+ }
+}
diff --git a/core/java/com/android/internal/app/chooser/TargetInfo.java b/core/java/com/android/internal/app/chooser/TargetInfo.java
new file mode 100644
index 000000000000..b59def174828
--- /dev/null
+++ b/core/java/com/android/internal/app/chooser/TargetInfo.java
@@ -0,0 +1,128 @@
+/*
+ * 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.internal.app.chooser;
+
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import com.android.internal.app.ResolverActivity;
+
+import java.util.List;
+
+/**
+ * A single target as represented in the chooser.
+ */
+public interface TargetInfo {
+ /**
+ * Get the resolved intent that represents this target. Note that this may not be the
+ * intent that will be launched by calling one of the <code>start</code> methods provided;
+ * this is the intent that will be credited with the launch.
+ *
+ * @return the resolved intent for this target
+ */
+ Intent getResolvedIntent();
+
+ /**
+ * Get the resolved component name that represents this target. Note that this may not
+ * be the component that will be directly launched by calling one of the <code>start</code>
+ * methods provided; this is the component that will be credited with the launch.
+ *
+ * @return the resolved ComponentName for this target
+ */
+ ComponentName getResolvedComponentName();
+
+ /**
+ * Start the activity referenced by this target.
+ *
+ * @param activity calling Activity performing the launch
+ * @param options ActivityOptions bundle
+ * @return true if the start completed successfully
+ */
+ boolean start(Activity activity, Bundle options);
+
+ /**
+ * Start the activity referenced by this target as if the ResolverActivity's caller
+ * was performing the start operation.
+ *
+ * @param activity calling Activity (actually) performing the launch
+ * @param options ActivityOptions bundle
+ * @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller
+ * @return true if the start completed successfully
+ */
+ boolean startAsCaller(ResolverActivity activity, Bundle options, int userId);
+
+ /**
+ * Start the activity referenced by this target as a given user.
+ *
+ * @param activity calling activity performing the launch
+ * @param options ActivityOptions bundle
+ * @param user handle for the user to start the activity as
+ * @return true if the start completed successfully
+ */
+ boolean startAsUser(Activity activity, Bundle options, UserHandle user);
+
+ /**
+ * Return the ResolveInfo about how and why this target matched the original query
+ * for available targets.
+ *
+ * @return ResolveInfo representing this target's match
+ */
+ ResolveInfo getResolveInfo();
+
+ /**
+ * Return the human-readable text label for this target.
+ *
+ * @return user-visible target label
+ */
+ CharSequence getDisplayLabel();
+
+ /**
+ * Return any extended info for this target. This may be used to disambiguate
+ * otherwise identical targets.
+ *
+ * @return human-readable disambig string or null if none present
+ */
+ CharSequence getExtendedInfo();
+
+ /**
+ * @return The drawable that should be used to represent this target including badge
+ * @param context
+ */
+ Drawable getDisplayIcon(Context context);
+
+ /**
+ * Clone this target with the given fill-in information.
+ */
+ TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
+
+ /**
+ * @return the list of supported source intents deduped against this single target
+ */
+ List<Intent> getAllSourceIntents();
+
+ /**
+ * @return true if this target can be selected by the user
+ */
+ boolean isSuspended();
+}
diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp
index bb5780558bdf..c0e4e1fe5e7a 100644
--- a/core/jni/fd_utils.cpp
+++ b/core/jni/fd_utils.cpp
@@ -59,6 +59,10 @@ FileDescriptorWhitelist* FileDescriptorWhitelist::Get() {
return instance_;
}
+static bool IsMemfd(const std::string& path) {
+ return android::base::StartsWith(path, "/memfd:");
+}
+
bool FileDescriptorWhitelist::IsAllowed(const std::string& path) const {
// Check the static whitelist path.
for (const auto& whitelist_path : kPathWhitelist) {
@@ -87,6 +91,11 @@ bool FileDescriptorWhitelist::IsAllowed(const std::string& path) const {
return true;
}
+ // In-memory files created through memfd_create are allowed.
+ if (IsMemfd(path)) {
+ return true;
+ }
+
// Whitelist files needed for Runtime Resource Overlay, like these:
// /system/vendor/overlay/framework-res.apk
// /system/vendor/overlay-subdir/pg/framework-res.apk
@@ -312,6 +321,11 @@ void FileDescriptorInfo::ReopenOrDetach(fail_fn_t fail_fn) const {
return DetachSocket(fail_fn);
}
+ // Children can directly use in-memory files created through memfd_create.
+ if (IsMemfd(file_path)) {
+ return;
+ }
+
// NOTE: This might happen if the file was unlinked after being opened.
// It's a common pattern in the case of temporary files and the like but
// we should not allow such usage from the zygote.
diff --git a/core/tests/coretests/src/android/app/NotificationHistoryTest.java b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
new file mode 100644
index 000000000000..08595bb43e06
--- /dev/null
+++ b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
@@ -0,0 +1,232 @@
+/*
+ * 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 android.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.NotificationHistory.HistoricalNotification;
+import android.graphics.drawable.Icon;
+import android.os.Parcel;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationHistoryTest {
+
+ private HistoricalNotification getHistoricalNotification(int index) {
+ return getHistoricalNotification("package" + index, index);
+ }
+
+ private HistoricalNotification getHistoricalNotification(String packageName, int index) {
+ String expectedChannelName = "channelName" + index;
+ String expectedChannelId = "channelId" + index;
+ int expectedUid = 1123456 + index;
+ int expectedUserId = 11 + index;
+ long expectedPostTime = 987654321 + index;
+ String expectedTitle = "title" + index;
+ String expectedText = "text" + index;
+ Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
+ index);
+
+ return new HistoricalNotification.Builder()
+ .setPackage(packageName)
+ .setChannelName(expectedChannelName)
+ .setChannelId(expectedChannelId)
+ .setUid(expectedUid)
+ .setUserId(expectedUserId)
+ .setPostedTimeMs(expectedPostTime)
+ .setTitle(expectedTitle)
+ .setText(expectedText)
+ .setIcon(expectedIcon)
+ .build();
+ }
+
+ @Test
+ public void testHistoricalNotificationBuilder() {
+ String expectedPackage = "package";
+ String expectedChannelName = "channelName";
+ String expectedChannelId = "channelId";
+ int expectedUid = 1123456;
+ int expectedUserId = 11;
+ long expectedPostTime = 987654321;
+ String expectedTitle = "title";
+ String expectedText = "text";
+ Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
+ android.R.drawable.btn_star);
+
+ HistoricalNotification n = new HistoricalNotification.Builder()
+ .setPackage(expectedPackage)
+ .setChannelName(expectedChannelName)
+ .setChannelId(expectedChannelId)
+ .setUid(expectedUid)
+ .setUserId(expectedUserId)
+ .setPostedTimeMs(expectedPostTime)
+ .setTitle(expectedTitle)
+ .setText(expectedText)
+ .setIcon(expectedIcon)
+ .build();
+
+ assertThat(n.getPackage()).isEqualTo(expectedPackage);
+ assertThat(n.getChannelName()).isEqualTo(expectedChannelName);
+ assertThat(n.getChannelId()).isEqualTo(expectedChannelId);
+ assertThat(n.getUid()).isEqualTo(expectedUid);
+ assertThat(n.getUserId()).isEqualTo(expectedUserId);
+ assertThat(n.getPostedTimeMs()).isEqualTo(expectedPostTime);
+ assertThat(n.getTitle()).isEqualTo(expectedTitle);
+ assertThat(n.getText()).isEqualTo(expectedText);
+ assertThat(expectedIcon.sameAs(n.getIcon())).isTrue();
+ }
+
+ @Test
+ public void testAddNotificationToWrite() {
+ NotificationHistory history = new NotificationHistory();
+ HistoricalNotification n = getHistoricalNotification(0);
+ HistoricalNotification n2 = getHistoricalNotification(1);
+
+ history.addNotificationToWrite(n2);
+ history.addNotificationToWrite(n);
+
+ assertThat(history.getNotificationsToWrite().size()).isEqualTo(2);
+ assertThat(history.getNotificationsToWrite().get(0)).isSameAs(n2);
+ assertThat(history.getNotificationsToWrite().get(1)).isSameAs(n);
+ assertThat(history.getHistoryCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void testPoolStringsFromNotifications() {
+ NotificationHistory history = new NotificationHistory();
+
+ List<String> expectedStrings = new ArrayList<>();
+ for (int i = 1; i <= 10; i++) {
+ HistoricalNotification n = getHistoricalNotification(i);
+ expectedStrings.add(n.getPackage());
+ expectedStrings.add(n.getChannelName());
+ expectedStrings.add(n.getChannelId());
+ history.addNotificationToWrite(n);
+ }
+
+ history.poolStringsFromNotifications();
+
+ assertThat(history.getPooledStringsToWrite().length).isEqualTo(expectedStrings.size());
+ String previous = null;
+ for (String actual : history.getPooledStringsToWrite()) {
+ assertThat(expectedStrings).contains(actual);
+
+ if (previous != null) {
+ assertThat(actual).isGreaterThan(previous);
+ }
+ previous = actual;
+ }
+ }
+
+ @Test
+ public void testAddPooledStrings() {
+ NotificationHistory history = new NotificationHistory();
+
+ List<String> expectedStrings = new ArrayList<>();
+ for (int i = 1; i <= 10; i++) {
+ HistoricalNotification n = getHistoricalNotification(i);
+ expectedStrings.add(n.getPackage());
+ expectedStrings.add(n.getChannelName());
+ expectedStrings.add(n.getChannelId());
+ history.addNotificationToWrite(n);
+ }
+
+ history.addPooledStrings(expectedStrings);
+
+ String[] actualStrings = history.getPooledStringsToWrite();
+ assertThat(actualStrings.length).isEqualTo(expectedStrings.size());
+ String previous = null;
+ for (String actual : actualStrings) {
+ assertThat(expectedStrings).contains(actual);
+
+ if (previous != null) {
+ assertThat(actual).isGreaterThan(previous);
+ }
+ previous = actual;
+ }
+ }
+
+ @Test
+ public void testRemoveNotificationsFromWrite() {
+ NotificationHistory history = new NotificationHistory();
+
+ List<HistoricalNotification> postRemoveExpectedEntries = new ArrayList<>();
+ List<String> postRemoveExpectedStrings = new ArrayList<>();
+ for (int i = 1; i <= 10; i++) {
+ HistoricalNotification n =
+ getHistoricalNotification((i % 2 == 0) ? "pkgEven" : "pkgOdd", i);
+
+ if (i % 2 == 0) {
+ postRemoveExpectedStrings.add(n.getPackage());
+ postRemoveExpectedStrings.add(n.getChannelName());
+ postRemoveExpectedStrings.add(n.getChannelId());
+ postRemoveExpectedEntries.add(n);
+ }
+
+ history.addNotificationToWrite(n);
+ }
+
+ history.poolStringsFromNotifications();
+
+ assertThat(history.getNotificationsToWrite().size()).isEqualTo(10);
+ // 2 package names and 10 * 2 unique channel names and ids
+ assertThat(history.getPooledStringsToWrite().length).isEqualTo(22);
+
+ history.removeNotificationsFromWrite("pkgOdd");
+
+
+ // 1 package names and 5 * 2 unique channel names and ids
+ assertThat(history.getPooledStringsToWrite().length).isEqualTo(11);
+ assertThat(history.getNotificationsToWrite())
+ .containsExactlyElementsIn(postRemoveExpectedEntries);
+ }
+
+ @Test
+ public void testParceling() {
+ NotificationHistory history = new NotificationHistory();
+
+ List<HistoricalNotification> expectedEntries = new ArrayList<>();
+ for (int i = 10; i >= 1; i--) {
+ HistoricalNotification n = getHistoricalNotification(i);
+ expectedEntries.add(n);
+ history.addNotificationToWrite(n);
+ }
+ history.poolStringsFromNotifications();
+
+ Parcel parcel = Parcel.obtain();
+ history.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ NotificationHistory parceledHistory = NotificationHistory.CREATOR.createFromParcel(parcel);
+
+ assertThat(parceledHistory.getHistoryCount()).isEqualTo(expectedEntries.size());
+
+ for (int i = 0; i < expectedEntries.size(); i++) {
+ assertThat(parceledHistory.hasNextNotification()).isTrue();
+
+ HistoricalNotification postParcelNotification = parceledHistory.getNextNotification();
+ assertThat(postParcelNotification).isEqualTo(expectedEntries.get(i));
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
new file mode 100644
index 000000000000..6ae7eb72fab2
--- /dev/null
+++ b/core/tests/coretests/src/android/graphics/TypefaceEqualsTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 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 android.graphics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.content.res.AssetManager;
+import android.graphics.fonts.Font;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TypefaceEqualsTest {
+ @Test
+ public void testFontEqualWithLocale() throws IOException {
+ final AssetManager am =
+ InstrumentationRegistry.getInstrumentation().getContext().getAssets();
+
+ Font masterFont = new Font.Builder(am, "fonts/a3em.ttf").build();
+
+ Font jaFont = new Font.Builder(masterFont.getBuffer(), new File("fonts/a3em.ttf"), "ja")
+ .build();
+ Font jaFont2 = new Font.Builder(masterFont.getBuffer(), new File("fonts/a3em.ttf"), "ja")
+ .build();
+ Font koFont = new Font.Builder(masterFont.getBuffer(), new File("fonts/a3em.ttf"), "ko")
+ .build();
+
+ assertEquals(jaFont, jaFont2);
+ assertNotEquals(jaFont, koFont);
+ assertNotEquals(jaFont, masterFont);
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
index 5ea91da98fd3..d427cbda7fb6 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
@@ -24,12 +24,12 @@ import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
-import static com.android.internal.app.ChooserActivity.CALLER_TARGET_SCORE_BOOST;
-import static com.android.internal.app.ChooserActivity.SHORTCUT_TARGET_SCORE_BOOST;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_DEFAULT;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import static com.android.internal.app.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST;
+import static com.android.internal.app.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST;
import static com.android.internal.app.ChooserWrapperActivity.sOverrides;
import static org.hamcrest.CoreMatchers.is;
@@ -69,6 +69,7 @@ import androidx.test.rule.ActivityTestRule;
import com.android.internal.R;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
+import com.android.internal.app.chooser.DisplayResolveInfo;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -819,17 +820,18 @@ public class ChooserActivityTest {
when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
- when(sOverrides.resolverListController.getScore(Mockito.isA(
- ResolverActivity.DisplayResolveInfo.class))).thenReturn(testBaseScore);
+ when(sOverrides.resolverListController.getScore(Mockito.isA(DisplayResolveInfo.class)))
+ .thenReturn(testBaseScore);
final ChooserWrapperActivity activity = mActivityRule
.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
- final ResolverActivity.DisplayResolveInfo testDri =
+ final DisplayResolveInfo testDri =
activity.createTestDisplayResolveInfo(sendIntent,
- ResolverDataProvider.createResolveInfo(3, 0), "testLabel", "testInfo", sendIntent);
- final ChooserActivity.ChooserListAdapter adapter = activity.getAdapter();
+ ResolverDataProvider.createResolveInfo(3, 0), "testLabel", "testInfo", sendIntent,
+ /* resolveInfoPresentationGetter */ null);
+ final ChooserListAdapter adapter = activity.getAdapter();
assertThat(adapter.getBaseScore(null, 0), is(CALLER_TARGET_SCORE_BOOST));
assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_DEFAULT), is(testBaseScore));
@@ -970,7 +972,8 @@ public class ChooserActivityTest {
ri,
"testLabel",
"testInfo",
- sendIntent),
+ sendIntent,
+ /* resolveInfoPresentationGetter */ null),
serviceTargets,
TARGET_TYPE_CHOOSER_TARGET)
);
@@ -1036,7 +1039,8 @@ public class ChooserActivityTest {
ri,
"testLabel",
"testInfo",
- sendIntent),
+ sendIntent,
+ /* resolveInfoPresentationGetter */ null),
serviceTargets,
TARGET_TYPE_CHOOSER_TARGET)
);
@@ -1097,7 +1101,8 @@ public class ChooserActivityTest {
ri,
"testLabel",
"testInfo",
- sendIntent),
+ sendIntent,
+ /* resolveInfoPresentationGetter */ null),
serviceTargets,
TARGET_TYPE_CHOOSER_TARGET)
);
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
index 1d567c73f376..03705d0599e5 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
@@ -18,6 +18,7 @@ package com.android.internal.app;
import static org.mockito.Mockito.mock;
+import android.annotation.Nullable;
import android.app.usage.UsageStatsManager;
import android.content.ContentResolver;
import android.content.Context;
@@ -30,6 +31,9 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Size;
+import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter;
+import com.android.internal.app.chooser.DisplayResolveInfo;
+import com.android.internal.app.chooser.TargetInfo;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -64,7 +68,7 @@ public class ChooserWrapperActivity extends ChooserActivity {
}
@Override
- public void safelyStartActivity(TargetInfo cti) {
+ public void safelyStartActivity(com.android.internal.app.chooser.TargetInfo cti) {
if (sOverrides.onSafelyStartCallback != null &&
sOverrides.onSafelyStartCallback.apply(cti)) {
return;
@@ -133,8 +137,10 @@ public class ChooserWrapperActivity extends ChooserActivity {
}
public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
- CharSequence pLabel, CharSequence pInfo, Intent pOrigIntent) {
- return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, pOrigIntent);
+ CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
+ @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, replacementIntent,
+ resolveInfoPresentationGetter);
}
/**
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index 0fa29bf4436a..a401e21df805 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -43,10 +43,10 @@ import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.R;
-import com.android.internal.app.ResolverActivity.ActivityInfoPresentationGetter;
-import com.android.internal.app.ResolverActivity.ResolveInfoPresentationGetter;
import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.ResolverDataProvider.PackageManagerMockedInfo;
+import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
+import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter;
import com.android.internal.widget.ResolverDrawerLayout;
import org.junit.Before;
@@ -83,7 +83,7 @@ public class ResolverActivityTest {
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
- Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
waitForIdle();
assertThat(activity.getAdapter().getCount(), is(2));
@@ -216,7 +216,7 @@ public class ResolverActivityTest {
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
- Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
waitForIdle();
// The other entry is filtered to the last used slot
@@ -254,7 +254,7 @@ public class ResolverActivityTest {
Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
- Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
waitForIdle();
// The other entry is filtered to the other profile slot
@@ -300,7 +300,7 @@ public class ResolverActivityTest {
.thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0));
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
- Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
waitForIdle();
// The other entry is filtered to the other profile slot
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
index 9082543e8ebc..39cc83c3bc43 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
@@ -19,10 +19,14 @@ package com.android.internal.app;
import static org.mockito.Mockito.mock;
import android.app.usage.UsageStatsManager;
+import android.content.Context;
+import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
-import androidx.test.espresso.idling.CountingIdlingResource;
+import com.android.internal.app.chooser.TargetInfo;
+import java.util.List;
import java.util.function.Function;
/*
@@ -31,15 +35,17 @@ import java.util.function.Function;
public class ResolverWrapperActivity extends ResolverActivity {
static final OverrideData sOverrides = new OverrideData();
private UsageStatsManager mUsm;
- private CountingIdlingResource mLabelIdlingResource =
- new CountingIdlingResource("LoadLabelTask");
- public CountingIdlingResource getLabelIdlingResource() {
- return mLabelIdlingResource;
+ @Override
+ public ResolverListAdapter createAdapter(Context context, List<Intent> payloadIntents,
+ Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed,
+ boolean useLayoutForBrowsables) {
+ return new ResolverWrapperAdapter(context, payloadIntents, initialIntents, rList,
+ filterLastUsed, createListController(), useLayoutForBrowsables, this);
}
- ResolveListAdapter getAdapter() {
- return mAdapter;
+ ResolverWrapperAdapter getAdapter() {
+ return (ResolverWrapperAdapter) mAdapter;
}
@Override
@@ -72,11 +78,6 @@ public class ResolverWrapperActivity extends ResolverActivity {
return super.getPackageManager();
}
- @Override
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelWrapperTask(info, holder);
- }
-
/**
* We cannot directly mock the activity created since instrumentation creates it.
* <p>
@@ -96,22 +97,4 @@ public class ResolverWrapperActivity extends ResolverActivity {
resolverListController = mock(ResolverListController.class);
}
}
-
- class LoadLabelWrapperTask extends LoadLabelTask {
-
- protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) {
- super(dri, holder);
- }
-
- @Override
- protected void onPreExecute() {
- mLabelIdlingResource.increment();
- }
-
- @Override
- protected void onPostExecute(CharSequence[] result) {
- super.onPostExecute(result);
- mLabelIdlingResource.decrement();
- }
- }
} \ No newline at end of file
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
new file mode 100644
index 000000000000..e41df4186a12
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
@@ -0,0 +1,71 @@
+/*
+ * 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.internal.app;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+
+import androidx.test.espresso.idling.CountingIdlingResource;
+
+import com.android.internal.app.chooser.DisplayResolveInfo;
+
+import java.util.List;
+
+public class ResolverWrapperAdapter extends ResolverListAdapter {
+
+ private CountingIdlingResource mLabelIdlingResource =
+ new CountingIdlingResource("LoadLabelTask");
+
+ public ResolverWrapperAdapter(Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList, boolean filterLastUsed,
+ ResolverListController resolverListController, boolean useLayoutForBrowsables,
+ ResolverListCommunicator resolverListCommunicator) {
+ super(context, payloadIntents, initialIntents, rList, filterLastUsed,
+ resolverListController,
+ useLayoutForBrowsables, resolverListCommunicator);
+ }
+
+ public CountingIdlingResource getLabelIdlingResource() {
+ return mLabelIdlingResource;
+ }
+
+ @Override
+ protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
+ return new LoadLabelWrapperTask(info, holder);
+ }
+
+ class LoadLabelWrapperTask extends LoadLabelTask {
+
+ protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) {
+ super(dri, holder);
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mLabelIdlingResource.increment();
+ }
+
+ @Override
+ protected void onPostExecute(CharSequence[] result) {
+ super.onPostExecute(result);
+ mLabelIdlingResource.decrement();
+ }
+ }
+}
diff --git a/graphics/java/android/graphics/fonts/Font.java b/graphics/java/android/graphics/fonts/Font.java
index 552088f7c478..ba96a06cc852 100644
--- a/graphics/java/android/graphics/fonts/Font.java
+++ b/graphics/java/android/graphics/fonts/Font.java
@@ -519,12 +519,13 @@ public final class Font {
}
Font f = (Font) o;
return mFontStyle.equals(f.mFontStyle) && f.mTtcIndex == mTtcIndex
- && Arrays.equals(f.mAxes, mAxes) && f.mBuffer.equals(mBuffer);
+ && Arrays.equals(f.mAxes, mAxes) && f.mBuffer.equals(mBuffer)
+ && Objects.equals(f.mLocaleList, mLocaleList);
}
@Override
public int hashCode() {
- return Objects.hash(mFontStyle, mTtcIndex, Arrays.hashCode(mAxes), mBuffer);
+ return Objects.hash(mFontStyle, mTtcIndex, Arrays.hashCode(mAxes), mBuffer, mLocaleList);
}
@Override
diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java
index bf7da23323a1..9723652b5bd3 100644
--- a/media/java/android/media/MediaRecorder.java
+++ b/media/java/android/media/MediaRecorder.java
@@ -337,9 +337,14 @@ public class MediaRecorder implements AudioRouting,
/**
* Audio source for capturing broadcast radio tuner output.
+ * Capturing the radio tuner output requires the
+ * {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT} permission.
+ * This permission is reserved for use by system components and is not available to
+ * third-party applications.
* @hide
*/
@SystemApi
+ @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT)
public static final int RADIO_TUNER = 1998;
/**
diff --git a/media/jni/audioeffect/Visualizer.h b/media/jni/audioeffect/Visualizer.h
index 8078e369ee82..d4672a95c6d8 100644
--- a/media/jni/audioeffect/Visualizer.h
+++ b/media/jni/audioeffect/Visualizer.h
@@ -73,7 +73,8 @@ public:
~Visualizer();
- virtual status_t setEnabled(bool enabled);
+ // Declared 'final' because we call this in ~Visualizer().
+ status_t setEnabled(bool enabled) final;
// maximum capture size in samples
static uint32_t getMaxCaptureSize() { return VISUALIZER_CAPTURE_SIZE_MAX; }
diff --git a/native/android/system_fonts.cpp b/native/android/system_fonts.cpp
index 9791da63359b..45f42f1b5dc6 100644
--- a/native/android/system_fonts.cpp
+++ b/native/android/system_fonts.cpp
@@ -16,6 +16,8 @@
#include <jni.h>
+#define LOG_TAG "SystemFont"
+
#include <android/font.h>
#include <android/font_matcher.h>
#include <android/system_fonts.h>
@@ -47,9 +49,14 @@ struct XmlDocDeleter {
using XmlCharUniquePtr = std::unique_ptr<xmlChar, XmlCharDeleter>;
using XmlDocUniquePtr = std::unique_ptr<xmlDoc, XmlDocDeleter>;
+struct ParserState {
+ xmlNode* mFontNode = nullptr;
+ XmlCharUniquePtr mLocale;
+};
+
struct ASystemFontIterator {
XmlDocUniquePtr mXmlDoc;
- xmlNode* mFontNode;
+ ParserState state;
// The OEM customization XML.
XmlDocUniquePtr mCustomizationXmlDoc;
@@ -97,6 +104,7 @@ std::string xmlTrim(const std::string& in) {
const xmlChar* FAMILY_TAG = BAD_CAST("family");
const xmlChar* FONT_TAG = BAD_CAST("font");
+const xmlChar* LOCALE_ATTR_NAME = BAD_CAST("lang");
xmlNode* firstElement(xmlNode* node, const xmlChar* tag) {
for (xmlNode* child = node->children; child; child = child->next) {
@@ -116,9 +124,9 @@ xmlNode* nextSibling(xmlNode* node, const xmlChar* tag) {
return nullptr;
}
-void copyFont(const XmlDocUniquePtr& xmlDoc, xmlNode* fontNode, AFont* out,
+void copyFont(const XmlDocUniquePtr& xmlDoc, const ParserState& state, AFont* out,
const std::string& pathPrefix) {
- const xmlChar* LOCALE_ATTR_NAME = BAD_CAST("lang");
+ xmlNode* fontNode = state.mFontNode;
XmlCharUniquePtr filePathStr(
xmlNodeListGetString(xmlDoc.get(), fontNode->xmlChildrenNode, 1));
out->mFilePath = pathPrefix + xmlTrim(
@@ -139,9 +147,10 @@ void copyFont(const XmlDocUniquePtr& xmlDoc, xmlNode* fontNode, AFont* out,
out->mCollectionIndex = indexStr ?
strtol(reinterpret_cast<const char*>(indexStr.get()), nullptr, 10) : 0;
- XmlCharUniquePtr localeStr(xmlGetProp(xmlDoc->parent, LOCALE_ATTR_NAME));
out->mLocale.reset(
- localeStr ? new std::string(reinterpret_cast<const char*>(localeStr.get())) : nullptr);
+ state.mLocale ?
+ new std::string(reinterpret_cast<const char*>(state.mLocale.get()))
+ : nullptr);
const xmlChar* TAG_ATTR_NAME = BAD_CAST("tag");
const xmlChar* STYLEVALUE_ATTR_NAME = BAD_CAST("stylevalue");
@@ -178,25 +187,27 @@ bool isFontFileAvailable(const std::string& filePath) {
return S_ISREG(st.st_mode);
}
-xmlNode* findFirstFontNode(const XmlDocUniquePtr& doc) {
+bool findFirstFontNode(const XmlDocUniquePtr& doc, ParserState* state) {
xmlNode* familySet = xmlDocGetRootElement(doc.get());
if (familySet == nullptr) {
- return nullptr;
+ return false;
}
xmlNode* family = firstElement(familySet, FAMILY_TAG);
if (family == nullptr) {
- return nullptr;
+ return false;
}
+ state->mLocale.reset(xmlGetProp(family, LOCALE_ATTR_NAME));
xmlNode* font = firstElement(family, FONT_TAG);
while (font == nullptr) {
family = nextSibling(family, FAMILY_TAG);
if (family == nullptr) {
- return nullptr;
+ return false;
}
font = firstElement(family, FONT_TAG);
}
- return font;
+ state->mFontNode = font;
+ return font != nullptr;
}
} // namespace
@@ -272,38 +283,38 @@ AFont* _Nonnull AFontMatcher_match(
return result.release();
}
-xmlNode* findNextFontNode(const XmlDocUniquePtr& xmlDoc, xmlNode* fontNode) {
- if (fontNode == nullptr) {
+bool findNextFontNode(const XmlDocUniquePtr& xmlDoc, ParserState* state) {
+ if (state->mFontNode == nullptr) {
if (!xmlDoc) {
- return nullptr; // Already at the end.
+ return false; // Already at the end.
} else {
// First time to query font.
- return findFirstFontNode(xmlDoc);
+ return findFirstFontNode(xmlDoc, state);
}
} else {
- xmlNode* nextNode = nextSibling(fontNode, FONT_TAG);
+ xmlNode* nextNode = nextSibling(state->mFontNode, FONT_TAG);
while (nextNode == nullptr) {
- xmlNode* family = nextSibling(fontNode->parent, FAMILY_TAG);
+ xmlNode* family = nextSibling(state->mFontNode->parent, FAMILY_TAG);
if (family == nullptr) {
break;
}
+ state->mLocale.reset(xmlGetProp(family, LOCALE_ATTR_NAME));
nextNode = firstElement(family, FONT_TAG);
}
- return nextNode;
+ state->mFontNode = nextNode;
+ return nextNode != nullptr;
}
}
AFont* ASystemFontIterator_next(ASystemFontIterator* ite) {
LOG_ALWAYS_FATAL_IF(ite == nullptr, "nullptr has passed as iterator argument");
if (ite->mXmlDoc) {
- ite->mFontNode = findNextFontNode(ite->mXmlDoc, ite->mFontNode);
- if (ite->mFontNode == nullptr) {
+ if (!findNextFontNode(ite->mXmlDoc, &ite->state)) {
// Reached end of the XML file. Continue OEM customization.
ite->mXmlDoc.reset();
- ite->mFontNode = nullptr;
} else {
std::unique_ptr<AFont> font = std::make_unique<AFont>();
- copyFont(ite->mXmlDoc, ite->mFontNode, font.get(), "/system/fonts/");
+ copyFont(ite->mXmlDoc, ite->state, font.get(), "/system/fonts/");
if (!isFontFileAvailable(font->mFilePath)) {
return ASystemFontIterator_next(ite);
}
@@ -312,15 +323,13 @@ AFont* ASystemFontIterator_next(ASystemFontIterator* ite) {
}
if (ite->mCustomizationXmlDoc) {
// TODO: Filter only customizationType="new-named-family"
- ite->mFontNode = findNextFontNode(ite->mCustomizationXmlDoc, ite->mFontNode);
- if (ite->mFontNode == nullptr) {
+ if (!findNextFontNode(ite->mCustomizationXmlDoc, &ite->state)) {
// Reached end of the XML file. Finishing
ite->mCustomizationXmlDoc.reset();
- ite->mFontNode = nullptr;
return nullptr;
} else {
std::unique_ptr<AFont> font = std::make_unique<AFont>();
- copyFont(ite->mCustomizationXmlDoc, ite->mFontNode, font.get(), "/product/fonts/");
+ copyFont(ite->mCustomizationXmlDoc, ite->state, font.get(), "/product/fonts/");
if (!isFontFileAvailable(font->mFilePath)) {
return ASystemFontIterator_next(ite);
}
@@ -351,7 +360,7 @@ bool AFont_isItalic(const AFont* font) {
const char* AFont_getLocale(const AFont* font) {
LOG_ALWAYS_FATAL_IF(font == nullptr, "nullptr has passed to font argument");
- return font->mLocale ? nullptr : font->mLocale->c_str();
+ return font->mLocale ? font->mLocale->c_str() : nullptr;
}
size_t AFont_getCollectionIndex(const AFont* font) {
diff --git a/packages/CarSystemUI/Android.bp b/packages/CarSystemUI/Android.bp
index 672879ae6e9d..b2451c91057c 100644
--- a/packages/CarSystemUI/Android.bp
+++ b/packages/CarSystemUI/Android.bp
@@ -63,6 +63,64 @@ android_library {
}
+android_library {
+ name: "CarSystemUI-tests",
+ manifest: "tests/AndroidManifest.xml",
+ resource_dirs: [
+ "tests/res",
+ "res-keyguard",
+ "res",
+ ],
+ srcs: [
+ "tests/src/**/*.java",
+ "src/**/*.java",
+ "src/**/I*.aidl",
+ ],
+ static_libs: [
+ "SystemUI-tests",
+ "CarNotificationLib",
+ "SystemUIPluginLib",
+ "SystemUISharedLib",
+ "SettingsLib",
+ "android.car.userlib",
+ "androidx.legacy_legacy-support-v4",
+ "androidx.recyclerview_recyclerview",
+ "androidx.preference_preference",
+ "androidx.appcompat_appcompat",
+ "androidx.mediarouter_mediarouter",
+ "androidx.palette_palette",
+ "androidx.legacy_legacy-preference-v14",
+ "androidx.leanback_leanback",
+ "androidx.slice_slice-core",
+ "androidx.slice_slice-view",
+ "androidx.slice_slice-builders",
+ "androidx.arch.core_core-runtime",
+ "androidx.lifecycle_lifecycle-extensions",
+ "SystemUI-tags",
+ "SystemUI-proto",
+ "metrics-helper-lib",
+ "androidx.test.rules", "hamcrest-library",
+ "mockito-target-inline-minus-junit4",
+ "testables",
+ "truth-prebuilt",
+ "dagger2-2.19",
+ "//external/kotlinc:kotlin-annotations",
+ ],
+ libs: [
+ "android.test.runner",
+ "telephony-common",
+ "android.test.base",
+ "android.car",
+ ],
+
+ aaptflags: [
+ "--extra-packages",
+ "com.android.systemui",
+ ],
+
+ plugins: ["dagger2-compiler-2.19"],
+}
+
android_app {
name: "CarSystemUI",
diff --git a/packages/CarSystemUI/res/values/config.xml b/packages/CarSystemUI/res/values/config.xml
index fe042fe2e17f..329225cf94fb 100644
--- a/packages/CarSystemUI/res/values/config.xml
+++ b/packages/CarSystemUI/res/values/config.xml
@@ -40,6 +40,21 @@
slots that may be reused for things like IME control. -->
<integer name="config_maxNotificationIcons">0</integer>
+ <!--
+ Initial alpha percent value for the background when the notification
+ shade is open. Should be a number between, and inclusive, 0 and 100.
+ If the number is 0, then the background alpha starts off fully
+ transparent. If the number if 100, then the background alpha starts off
+ fully opaque. -->
+ <integer name="config_initialNotificationBackgroundAlpha">0</integer>
+ <!--
+ Final alpha percent value for the background when the notification
+ shade is fully open. Should be a number between, and inclusive, 0 and
+ 100. If this value is smaller than
+ config_initialNotificationBackgroundAlpha, the background will default
+ to a constant alpha percent value using the initial alpha. -->
+ <integer name="config_finalNotificationBackgroundAlpha">100</integer>
+
<!-- SystemUI Services: The classes of the stuff to start. -->
<string-array name="config_systemUIServiceComponents" translatable="false">
<item>com.android.systemui.util.NotificationChannels</item>
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java
index 63bc66afddf8..98b91ebd8038 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java
@@ -25,6 +25,7 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.view.Display;
import android.view.Gravity;
+import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -291,7 +292,8 @@ public class CarNavigationBar extends SystemUI implements CommandQueue.Callbacks
}
boolean isKeyboardVisible = (vis & InputMethodService.IME_VISIBLE) != 0;
- mCarNavigationBarController.setBottomWindowVisibility(!isKeyboardVisible);
+ mCarNavigationBarController.setBottomWindowVisibility(
+ isKeyboardVisible ? View.GONE : View.VISIBLE);
}
private void updateNavBarForKeyguardContent() {
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java
index f59f886d487b..6bed69bdee88 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java
+++ b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java
@@ -98,31 +98,30 @@ public class CarNavigationBarController {
}
/** Toggles the bottom nav bar visibility. */
- public boolean setBottomWindowVisibility(boolean isVisible) {
- return setWindowVisibility(getBottomWindow(), isVisible);
+ public boolean setBottomWindowVisibility(@View.Visibility int visibility) {
+ return setWindowVisibility(getBottomWindow(), visibility);
}
/** Toggles the left nav bar visibility. */
- public boolean setLeftWindowVisibility(boolean isVisible) {
- return setWindowVisibility(getLeftWindow(), isVisible);
+ public boolean setLeftWindowVisibility(@View.Visibility int visibility) {
+ return setWindowVisibility(getLeftWindow(), visibility);
}
/** Toggles the right nav bar visibility. */
- public boolean setRightWindowVisibility(boolean isVisible) {
- return setWindowVisibility(getRightWindow(), isVisible);
+ public boolean setRightWindowVisibility(@View.Visibility int visibility) {
+ return setWindowVisibility(getRightWindow(), visibility);
}
- private boolean setWindowVisibility(ViewGroup window, boolean isVisible) {
+ private boolean setWindowVisibility(ViewGroup window, @View.Visibility int visibility) {
if (window == null) {
return false;
}
- int newVisibility = isVisible ? View.VISIBLE : View.GONE;
- if (window.getVisibility() == newVisibility) {
+ if (window.getVisibility() == visibility) {
return false;
}
- window.setVisibility(newVisibility);
+ window.setVisibility(visibility);
return true;
}
@@ -228,6 +227,63 @@ public class CarNavigationBarController {
}
}
+ /**
+ * Shows all of the keyguard specific buttons on the valid instances of
+ * {@link CarNavigationBarView}.
+ */
+ public void showAllKeyguardButtons(boolean isSetUp) {
+ checkAllBars(isSetUp);
+ if (mTopView != null) {
+ mTopView.showKeyguardButtons();
+ }
+ if (mBottomView != null) {
+ mBottomView.showKeyguardButtons();
+ }
+ if (mLeftView != null) {
+ mLeftView.showKeyguardButtons();
+ }
+ if (mRightView != null) {
+ mRightView.showKeyguardButtons();
+ }
+ }
+
+ /**
+ * Hides all of the keyguard specific buttons on the valid instances of
+ * {@link CarNavigationBarView}.
+ */
+ public void hideAllKeyguardButtons(boolean isSetUp) {
+ checkAllBars(isSetUp);
+ if (mTopView != null) {
+ mTopView.hideKeyguardButtons();
+ }
+ if (mBottomView != null) {
+ mBottomView.hideKeyguardButtons();
+ }
+ if (mLeftView != null) {
+ mLeftView.hideKeyguardButtons();
+ }
+ if (mRightView != null) {
+ mRightView.hideKeyguardButtons();
+ }
+ }
+
+ /** Toggles whether the notifications icon has an unseen indicator or not. */
+ public void toggleAllNotificationsUnseenIndicator(boolean isSetUp, boolean hasUnseen) {
+ checkAllBars(isSetUp);
+ if (mTopView != null) {
+ mTopView.toggleNotificationUnseenIndicator(hasUnseen);
+ }
+ if (mBottomView != null) {
+ mBottomView.toggleNotificationUnseenIndicator(hasUnseen);
+ }
+ if (mLeftView != null) {
+ mLeftView.toggleNotificationUnseenIndicator(hasUnseen);
+ }
+ if (mRightView != null) {
+ mRightView.toggleNotificationUnseenIndicator(hasUnseen);
+ }
+ }
+
/** Interface for controlling the notifications shade. */
public interface NotificationsShadeController {
/** Toggles the visibility of the notifications shade. */
@@ -244,4 +300,11 @@ public class CarNavigationBarController {
}
}
}
+
+ private void checkAllBars(boolean isSetUp) {
+ mTopView = getTopBar(isSetUp);
+ mBottomView = getBottomBar(isSetUp);
+ mLeftView = getLeftBar(isSetUp);
+ mRightView = getRightBar(isSetUp);
+ }
}
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarView.java b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarView.java
index 24f8b74eed61..c2455088a52b 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarView.java
+++ b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarView.java
@@ -24,6 +24,7 @@ import android.widget.LinearLayout;
import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.navigationbar.car.CarNavigationBarController.NotificationsShadeController;
import com.android.systemui.statusbar.phone.StatusBarIconController;
/**
@@ -35,7 +36,7 @@ import com.android.systemui.statusbar.phone.StatusBarIconController;
public class CarNavigationBarView extends LinearLayout {
private View mNavButtons;
private CarNavigationButton mNotificationsButton;
- private CarNavigationBarController.NotificationsShadeController mNotificationsShadeController;
+ private NotificationsShadeController mNotificationsShadeController;
private Context mContext;
private View mLockScreenButtons;
// used to wire in open/close gestures for notifications
@@ -81,13 +82,18 @@ public class CarNavigationBarView extends LinearLayout {
return super.onInterceptTouchEvent(ev);
}
- public void setNotificationsPanelController(
- CarNavigationBarController.NotificationsShadeController controller) {
+ /** Sets the notifications panel controller. */
+ public void setNotificationsPanelController(NotificationsShadeController controller) {
mNotificationsShadeController = controller;
}
+ /** Gets the notifications panel controller. */
+ public NotificationsShadeController getNotificationsPanelController() {
+ return mNotificationsShadeController;
+ }
+
/**
- * Set a touch listener that will be called from onInterceptTouchEvent and onTouchEvent
+ * Sets a touch listener that will be called from onInterceptTouchEvent and onTouchEvent
*
* @param statusBarWindowTouchListener The listener to call from touch and intercept touch
*/
@@ -95,6 +101,11 @@ public class CarNavigationBarView extends LinearLayout {
mStatusBarWindowTouchListener = statusBarWindowTouchListener;
}
+ /** Gets the touch listener that will be called from onInterceptTouchEvent and onTouchEvent. */
+ public OnTouchListener getStatusBarWindowTouchListener() {
+ return mStatusBarWindowTouchListener;
+ }
+
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mStatusBarWindowTouchListener != null) {
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationButton.java b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationButton.java
index 40823abaaead..922bfffcfa22 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationButton.java
+++ b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationButton.java
@@ -150,6 +150,11 @@ public class CarNavigationButton extends com.android.keyguard.AlphaOptimizedImag
updateImage();
}
+ /** Gets whether the icon is in an unseen state. */
+ public boolean getUnseen() {
+ return mHasUnseen;
+ }
+
private void updateImage() {
if (mHasUnseen) {
setImageResource(mSelected ? UNSEEN_SELECTED_ICON_RESOURCE_ID
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index a1a6ab49ee0a..ce763b900b9a 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -32,6 +32,7 @@ import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.os.PowerManager;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
@@ -107,6 +108,8 @@ import com.android.systemui.statusbar.phone.AutoHideController;
import com.android.systemui.statusbar.phone.BiometricUnlockController;
import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.DozeScrimController;
+import com.android.systemui.statusbar.phone.DozeServiceHost;
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.LightBarController;
@@ -155,6 +158,9 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
private float mOpeningVelocity = DEFAULT_FLING_VELOCITY;
private float mClosingVelocity = DEFAULT_FLING_VELOCITY;
+ private float mBackgroundAlphaDiff;
+ private float mInitialBackgroundAlpha;
+
private FullscreenUserSwitcher mFullscreenUserSwitcher;
private CarBatteryController mCarBatteryController;
@@ -162,13 +168,7 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
private Drawable mNotificationPanelBackground;
private ViewGroup mTopNavigationBarContainer;
- private ViewGroup mNavigationBarWindow;
- private ViewGroup mLeftNavigationBarWindow;
- private ViewGroup mRightNavigationBarWindow;
private CarNavigationBarView mTopNavigationBarView;
- private CarNavigationBarView mNavigationBarView;
- private CarNavigationBarView mLeftNavigationBarView;
- private CarNavigationBarView mRightNavigationBarView;
private final Object mQueueLock = new Object();
private final CarNavigationBarController mCarNavigationBarController;
@@ -297,6 +297,9 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
ScrimController scrimController,
Lazy<LockscreenWallpaper> lockscreenWallpaperLazy,
Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
+ DozeServiceHost dozeServiceHost,
+ PowerManager powerManager,
+ DozeScrimController dozeScrimController,
/* Car Settings injected components. */
CarNavigationBarController carNavigationBarController) {
@@ -360,7 +363,10 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
dozeParameters,
scrimController,
lockscreenWallpaperLazy,
- biometricUnlockControllerLazy);
+ biometricUnlockControllerLazy,
+ dozeServiceHost,
+ powerManager,
+ dozeScrimController);
mScrimController = scrimController;
mCarNavigationBarController = carNavigationBarController;
}
@@ -377,6 +383,25 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
mScreenLifecycle = Dependency.get(ScreenLifecycle.class);
mScreenLifecycle.addObserver(mScreenObserver);
+ // Notification bar related setup.
+ mInitialBackgroundAlpha = (float) mContext.getResources().getInteger(
+ R.integer.config_initialNotificationBackgroundAlpha) / 100;
+ if (mInitialBackgroundAlpha < 0 || mInitialBackgroundAlpha > 100) {
+ throw new RuntimeException(
+ "Unable to setup notification bar due to incorrect initial background alpha"
+ + " percentage");
+ }
+ float finalBackgroundAlpha = Math.max(
+ mInitialBackgroundAlpha,
+ (float) mContext.getResources().getInteger(
+ R.integer.config_finalNotificationBackgroundAlpha) / 100);
+ if (finalBackgroundAlpha < 0 || finalBackgroundAlpha > 100) {
+ throw new RuntimeException(
+ "Unable to setup notification bar due to incorrect final background alpha"
+ + " percentage");
+ }
+ mBackgroundAlphaDiff = finalBackgroundAlpha - mInitialBackgroundAlpha;
+
super.start();
mNotificationPanel.setScrollingEnabled(true);
@@ -394,12 +419,12 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
new DeviceProvisionedController.DeviceProvisionedListener() {
@Override
public void onUserSetupChanged() {
- mHandler.post(() -> restartNavBarsIfNecessary());
+ mHandler.post(() -> resetSystemBarsIfNecessary());
}
@Override
public void onUserSwitched() {
- mHandler.post(() -> restartNavBarsIfNecessary());
+ mHandler.post(() -> resetSystemBarsIfNecessary());
}
});
@@ -413,11 +438,11 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
mSwitchToGuestTimer = new SwitchToGuestTimer(mContext);
}
- private void restartNavBarsIfNecessary() {
+ private void resetSystemBarsIfNecessary() {
boolean currentUserSetup = mDeviceProvisionedController.isCurrentUserSetup();
if (mDeviceIsSetUpForUser != currentUserSetup) {
mDeviceIsSetUpForUser = currentUserSetup;
- restartNavBars();
+ resetSystemBars();
}
}
@@ -425,19 +450,9 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
* Remove all content from navbars and rebuild them. Used to allow for different nav bars
* before and after the device is provisioned. . Also for change of density and font size.
*/
- private void restartNavBars() {
+ private void resetSystemBars() {
mCarFacetButtonController.removeAll();
- if (mNavigationBarWindow != null) {
- mNavigationBarView = null;
- }
- if (mLeftNavigationBarWindow != null) {
- mLeftNavigationBarView = null;
- }
- if (mRightNavigationBarWindow != null) {
- mRightNavigationBarView = null;
- }
-
buildNavBarContent();
// CarFacetButtonController was reset therefore we need to re-add the status bar elements
// to the controller.
@@ -449,51 +464,22 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
* the full screen user selector is shown.
*/
void setNavBarVisibility(@View.Visibility int visibility) {
- if (mNavigationBarWindow != null) {
- mNavigationBarWindow.setVisibility(visibility);
- }
- if (mLeftNavigationBarWindow != null) {
- mLeftNavigationBarWindow.setVisibility(visibility);
- }
- if (mRightNavigationBarWindow != null) {
- mRightNavigationBarWindow.setVisibility(visibility);
- }
+ mCarNavigationBarController.setBottomWindowVisibility(visibility);
+ mCarNavigationBarController.setLeftWindowVisibility(visibility);
+ mCarNavigationBarController.setRightWindowVisibility(visibility);
}
@Override
public boolean hideKeyguard() {
boolean result = super.hideKeyguard();
- if (mNavigationBarView != null) {
- mNavigationBarView.hideKeyguardButtons();
- }
- if (mLeftNavigationBarView != null) {
- mLeftNavigationBarView.hideKeyguardButtons();
- }
- if (mRightNavigationBarView != null) {
- mRightNavigationBarView.hideKeyguardButtons();
- }
+ mCarNavigationBarController.hideAllKeyguardButtons(mDeviceIsSetUpForUser);
return result;
}
@Override
public void showKeyguard() {
super.showKeyguard();
- updateNavBarForKeyguardContent();
- }
-
- /**
- * Switch to the keyguard applicable content contained in the nav bars
- */
- private void updateNavBarForKeyguardContent() {
- if (mNavigationBarView != null) {
- mNavigationBarView.showKeyguardButtons();
- }
- if (mLeftNavigationBarView != null) {
- mLeftNavigationBarView.showKeyguardButtons();
- }
- if (mRightNavigationBarView != null) {
- mRightNavigationBarView.showKeyguardButtons();
- }
+ mCarNavigationBarController.showAllKeyguardButtons(mDeviceIsSetUpForUser);
}
@Override
@@ -597,20 +583,11 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
mNotificationDataManager = new NotificationDataManager();
mNotificationDataManager.setOnUnseenCountUpdateListener(
() -> {
- if (mNavigationBarView != null && mNotificationDataManager != null) {
- Boolean hasUnseen =
+ if (mNotificationDataManager != null) {
+ boolean hasUnseen =
mNotificationDataManager.getUnseenNotificationCount() > 0;
- if (mNavigationBarView != null) {
- mNavigationBarView.toggleNotificationUnseenIndicator(hasUnseen);
- }
-
- if (mLeftNavigationBarView != null) {
- mLeftNavigationBarView.toggleNotificationUnseenIndicator(hasUnseen);
- }
-
- if (mRightNavigationBarView != null) {
- mRightNavigationBarView.toggleNotificationUnseenIndicator(hasUnseen);
- }
+ mCarNavigationBarController.toggleAllNotificationsUnseenIndicator(
+ mDeviceIsSetUpForUser, hasUnseen);
}
});
@@ -864,37 +841,27 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
@Override
protected void createNavigationBar(@Nullable RegisterStatusBarResult result) {
- buildNavBarWindows();
+ mTopNavigationBarContainer = mStatusBarWindow
+ .findViewById(R.id.car_top_navigation_bar_container);
+
buildNavBarContent();
}
private void buildNavBarContent() {
buildTopBar();
- mNavigationBarView = mCarNavigationBarController.getBottomBar(mDeviceIsSetUpForUser);
mCarNavigationBarController.registerBottomBarTouchListener(
mNavBarNotificationTouchListener);
- mLeftNavigationBarView = mCarNavigationBarController.getLeftBar(mDeviceIsSetUpForUser);
mCarNavigationBarController.registerLeftBarTouchListener(
mNavBarNotificationTouchListener);
- mRightNavigationBarView = mCarNavigationBarController.getLeftBar(mDeviceIsSetUpForUser);
mCarNavigationBarController.registerRightBarTouchListener(
mNavBarNotificationTouchListener);
mCarNavigationBarController.registerNotificationController(() -> togglePanel());
}
- private void buildNavBarWindows() {
- mTopNavigationBarContainer = mStatusBarWindow
- .findViewById(R.id.car_top_navigation_bar_container);
-
- mNavigationBarWindow = mCarNavigationBarController.getBottomWindow();
- mLeftNavigationBarWindow = mCarNavigationBarController.getLeftWindow();
- mRightNavigationBarWindow = mCarNavigationBarController.getRightWindow();
- }
-
private void buildTopBar() {
mTopNavigationBarContainer.removeAllViews();
mTopNavigationBarView = mCarNavigationBarController.getTopBar(mDeviceIsSetUpForUser);
@@ -1068,7 +1035,7 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
@Override
public void onDensityOrFontScaleChanged() {
super.onDensityOrFontScaleChanged();
- restartNavBars();
+ resetSystemBars();
// Need to update the background on density changed in case the change was due to night
// mode.
mNotificationPanelBackground = getDefaultWallpaper();
@@ -1096,17 +1063,22 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt
mHandleBar.setTranslationY(height - mHandleBar.getHeight() - lp.bottomMargin);
}
if (mNotificationView.getHeight() > 0) {
- // Calculates the alpha value for the background based on how much of the notification
- // shade is visible to the user. When the notification shade is completely open then
- // alpha value will be 1.
- float alpha = (float) height / mNotificationView.getHeight();
Drawable background = mNotificationView.getBackground().mutate();
-
- background.setAlpha((int) (alpha * 255));
+ background.setAlpha((int) (getBackgroundAlpha(height) * 255));
mNotificationView.setBackground(background);
}
}
+ /**
+ * Calculates the alpha value for the background based on how much of the notification
+ * shade is visible to the user. When the notification shade is completely open then
+ * alpha value will be 1.
+ */
+ private float getBackgroundAlpha(int height) {
+ return mInitialBackgroundAlpha +
+ ((float) height / mNotificationView.getHeight() * mBackgroundAlphaDiff);
+ }
+
@Override
public void onConfigChanged(Configuration newConfig) {
super.onConfigChanged(newConfig);
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserGridRecyclerView.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserGridRecyclerView.java
index 3b482599b2a0..05657fff70e0 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserGridRecyclerView.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserGridRecyclerView.java
@@ -32,7 +32,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
import android.content.res.Resources;
-import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.UserHandle;
@@ -67,6 +66,7 @@ public class UserGridRecyclerView extends RecyclerView {
private CarUserManagerHelper mCarUserManagerHelper;
private UserManager mUserManager;
private Context mContext;
+ private UserIconProvider mUserIconProvider;
private final BroadcastReceiver mUserUpdateReceiver = new BroadcastReceiver() {
@Override
@@ -80,6 +80,7 @@ public class UserGridRecyclerView extends RecyclerView {
mContext = context;
mCarUserManagerHelper = new CarUserManagerHelper(mContext);
mUserManager = UserManager.get(mContext);
+ mUserIconProvider = new UserIconProvider();
addItemDecoration(new ItemSpacingDecoration(mContext.getResources().getDimensionPixelSize(
R.dimen.car_user_switcher_vertical_spacing_between_users)));
@@ -252,9 +253,7 @@ public class UserGridRecyclerView extends RecyclerView {
@Override
public void onBindViewHolder(UserAdapterViewHolder holder, int position) {
UserRecord userRecord = mUsers.get(position);
- RoundedBitmapDrawable circleIcon = RoundedBitmapDrawableFactory.create(mRes,
- getUserRecordIcon(userRecord));
- circleIcon.setCircular(true);
+ RoundedBitmapDrawable circleIcon = getCircularUserRecordIcon(userRecord);
holder.mUserAvatarImageView.setImageDrawable(circleIcon);
holder.mUserNameTextView.setText(userRecord.mInfo.name);
@@ -336,17 +335,20 @@ public class UserGridRecyclerView extends RecyclerView {
}
}
- private Bitmap getUserRecordIcon(UserRecord userRecord) {
+ private RoundedBitmapDrawable getCircularUserRecordIcon(UserRecord userRecord) {
+ Resources resources = mContext.getResources();
+ RoundedBitmapDrawable circleIcon;
if (userRecord.mIsStartGuestSession) {
- return mCarUserManagerHelper.getGuestDefaultIcon();
- }
-
- if (userRecord.mIsAddUser) {
- return UserIcons.convertToBitmap(mContext
- .getDrawable(R.drawable.car_add_circle_round));
+ circleIcon = mUserIconProvider.getRoundedGuestDefaultIcon(resources);
+ } else if (userRecord.mIsAddUser) {
+ circleIcon = RoundedBitmapDrawableFactory.create(mRes, UserIcons.convertToBitmap(
+ mContext.getDrawable(R.drawable.car_add_circle_round)));
+ circleIcon.setCircular(true);
+ } else {
+ circleIcon = mUserIconProvider.getRoundedUserIcon(userRecord.mInfo, mContext);
}
- return mCarUserManagerHelper.getUserIcon(userRecord.mInfo);
+ return circleIcon;
}
@Override
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserIconProvider.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserIconProvider.java
new file mode 100644
index 000000000000..9464eab2085b
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/UserIconProvider.java
@@ -0,0 +1,116 @@
+/*
+ * 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;
+
+import android.annotation.UserIdInt;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
+import com.android.internal.util.UserIcons;
+import com.android.systemui.R;
+
+/**
+ * Simple class for providing icons for users.
+ */
+public class UserIconProvider {
+ /**
+ * Gets a scaled rounded icon for the given user. If a user does not have an icon saved, this
+ * method will default to a generic icon and update UserManager to use that icon.
+ *
+ * @param userInfo User for which the icon is requested.
+ * @param context Context to use for resources
+ * @return {@link RoundedBitmapDrawable} representing the icon for the user.
+ */
+ public RoundedBitmapDrawable getRoundedUserIcon(UserInfo userInfo, Context context) {
+ UserManager userManager = UserManager.get(context);
+ Resources res = context.getResources();
+ Bitmap icon = userManager.getUserIcon(userInfo.id);
+
+ if (icon == null) {
+ icon = assignDefaultIcon(userManager, res, userInfo);
+ }
+
+ return createScaledRoundIcon(res, icon);
+ }
+
+ /** Returns a scaled, rounded, default icon for the Guest user */
+ public RoundedBitmapDrawable getRoundedGuestDefaultIcon(Resources resources) {
+ return createScaledRoundIcon(resources, getGuestUserDefaultIcon(resources));
+ }
+
+ private RoundedBitmapDrawable createScaledRoundIcon(Resources resources, Bitmap icon) {
+ BitmapDrawable scaledIcon = scaleUserIcon(resources, icon);
+ RoundedBitmapDrawable circleIcon =
+ RoundedBitmapDrawableFactory.create(resources, scaledIcon.getBitmap());
+ circleIcon.setCircular(true);
+ return circleIcon;
+ }
+
+ /**
+ * Returns a {@link Drawable} for the given {@code icon} scaled to the appropriate size.
+ */
+ private static BitmapDrawable scaleUserIcon(Resources res, Bitmap icon) {
+ int desiredSize = res.getDimensionPixelSize(R.dimen.car_primary_icon_size);
+ Bitmap scaledIcon =
+ Bitmap.createScaledBitmap(icon, desiredSize, desiredSize, /*filter=*/ true);
+ return new BitmapDrawable(res, scaledIcon);
+ }
+
+ /**
+ * Assigns a default icon to a user according to the user's id. Handles Guest icon and non-guest
+ * user icons.
+ *
+ * @param userManager {@link UserManager} to set user icon
+ * @param resources {@link Resources} to grab icons from
+ * @param userInfo User whose avatar is set to default icon.
+ * @return Bitmap of the user icon.
+ */
+ private Bitmap assignDefaultIcon(
+ UserManager userManager, Resources resources, UserInfo userInfo) {
+ Bitmap bitmap = userInfo.isGuest()
+ ? getGuestUserDefaultIcon(resources)
+ : getUserDefaultIcon(resources, userInfo.id);
+ userManager.setUserIcon(userInfo.id, bitmap);
+ return bitmap;
+ }
+
+ /**
+ * Gets a bitmap representing the user's default avatar.
+ *
+ * @param resources The resources to pull from
+ * @param id The id of the user to get the icon for. Pass {@link UserHandle#USER_NULL} for
+ * Guest user.
+ * @return Default user icon
+ */
+ private Bitmap getUserDefaultIcon(Resources resources, @UserIdInt int id) {
+ return UserIcons.convertToBitmap(
+ UserIcons.getDefaultUserIcon(resources, id, /* light= */ false));
+ }
+
+ private Bitmap getGuestUserDefaultIcon(Resources resources) {
+ return getUserDefaultIcon(resources, UserHandle.USER_NULL);
+ }
+}
diff --git a/packages/CarSystemUI/tests/Android.mk b/packages/CarSystemUI/tests/Android.mk
new file mode 100644
index 000000000000..1366568c3a66
--- /dev/null
+++ b/packages/CarSystemUI/tests/Android.mk
@@ -0,0 +1,88 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_USE_AAPT2 := true
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_JACK_FLAGS := --multi-dex native
+LOCAL_DX_FLAGS := --multi-dex
+
+LOCAL_PACKAGE_NAME := CarSystemUITests
+LOCAL_PRIVATE_PLATFORM_APIS := true
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+ CarSystemUI-tests
+
+LOCAL_MULTILIB := both
+
+LOCAL_JNI_SHARED_LIBRARIES := \
+ libdexmakerjvmtiagent \
+ libmultiplejvmtiagentsinterferenceagent
+
+LOCAL_JAVA_LIBRARIES := \
+ android.test.runner \
+ telephony-common \
+ android.test.base \
+
+LOCAL_AAPT_FLAGS := --extra-packages com.android.systemui
+
+# sign this with platform cert, so this test is allowed to inject key events into
+# UI it doesn't own. This is necessary to allow screenshots to be taken
+LOCAL_CERTIFICATE := platform
+
+# Provide jack a list of classes to exclude from code coverage.
+# This is needed because the CarSystemUITests compile CarSystemUI source directly, rather than using
+# LOCAL_INSTRUMENTATION_FOR := CarSystemUI.
+#
+# We want to exclude the test classes from code coverage measurements, but they share the same
+# package as the rest of SystemUI so they can't be easily filtered by package name.
+#
+# Generate a comma separated list of patterns based on the test source files under src/
+# SystemUI classes are in ../src/ so they won't be excluded.
+# Example:
+# Input files: src/com/android/systemui/Test.java src/com/android/systemui/AnotherTest.java
+# Generated exclude list: com.android.systemui.Test*,com.android.systemui.AnotherTest*
+
+# Filter all src files under src/ to just java files
+local_java_files := $(filter %.java,$(call all-java-files-under, src))
+
+# Transform java file names into full class names.
+# This only works if the class name matches the file name and the directory structure
+# matches the package.
+local_classes := $(subst /,.,$(patsubst src/%.java,%,$(local_java_files)))
+local_comma := ,
+local_empty :=
+local_space := $(local_empty) $(local_empty)
+
+# Convert class name list to jacoco exclude list
+# This appends a * to all classes and replace the space separators with commas.
+jacoco_exclude := $(subst $(space),$(comma),$(patsubst %,%*,$(local_classes)))
+
+LOCAL_JACK_COVERAGE_INCLUDE_FILTER := com.android.systemui.*,com.android.keyguard.*
+LOCAL_JACK_COVERAGE_EXCLUDE_FILTER := $(jacoco_exclude)
+
+ifeq ($(EXCLUDE_SYSTEMUI_TESTS),)
+ include $(BUILD_PACKAGE)
+endif
+
+# Reset variables
+local_java_files :=
+local_classes :=
+local_comma :=
+local_space :=
+jacoco_exclude :=
diff --git a/packages/CarSystemUI/tests/AndroidManifest.xml b/packages/CarSystemUI/tests/AndroidManifest.xml
new file mode 100644
index 000000000000..a74bb56d8d75
--- /dev/null
+++ b/packages/CarSystemUI/tests/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:sharedUserId="android.uid.system"
+ package="com.android.systemui.tests">
+
+ <application android:debuggable="true" android:largeHeap="true">
+ <uses-library android:name="android.test.runner" />
+
+ <provider
+ android:name="androidx.lifecycle.ProcessLifecycleOwnerInitializer"
+ tools:replace="android:authorities"
+ android:authorities="${applicationId}.lifecycle-tests"
+ android:exported="false"
+ android:enabled="false"
+ android:multiprocess="true" />
+ </application>
+
+ <instrumentation android:name="android.testing.TestableInstrumentation"
+ android:targetPackage="com.android.systemui.tests"
+ android:label="Tests for CarSystemUI">
+ </instrumentation>
+</manifest>
diff --git a/packages/CarSystemUI/tests/AndroidTest.xml b/packages/CarSystemUI/tests/AndroidTest.xml
new file mode 100644
index 000000000000..8685632f2b63
--- /dev/null
+++ b/packages/CarSystemUI/tests/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<configuration description="Runs Tests for CarSystemUI.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="CarSystemUITests.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="framework-base-presubmit" />
+ <option name="test-tag" value="CarSystemUITests" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.systemui.tests" />
+ <option name="runner" value="android.testing.TestableInstrumentation" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/packages/CarSystemUI/tests/res/values/config.xml b/packages/CarSystemUI/tests/res/values/config.xml
new file mode 100644
index 000000000000..0d08ac26d962
--- /dev/null
+++ b/packages/CarSystemUI/tests/res/values/config.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<resources>
+ <!-- Configure which system ui bars should be displayed.
+ These can be overwritten within the tests. -->
+ <bool name="config_enableLeftNavigationBar">false</bool>
+ <bool name="config_enableRightNavigationBar">false</bool>
+ <bool name="config_enableBottomNavigationBar">false</bool>
+</resources>
diff --git a/packages/CarSystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java b/packages/CarSystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java
new file mode 100644
index 000000000000..fe59cbf20a13
--- /dev/null
+++ b/packages/CarSystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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;
+
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+import android.testing.AndroidTestingRunner;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
+import androidx.test.internal.runner.ClassPathScanner;
+import androidx.test.internal.runner.ClassPathScanner.ChainedClassNameFilter;
+import androidx.test.internal.runner.ClassPathScanner.ExternalClassNameFilter;
+
+import com.android.systemui.SysuiBaseFragmentTest;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * This is named AAAPlusPlusVerifySysuiRequiredTestPropertiesTest for two reasons.
+ * a) Its so awesome it deserves an AAA++
+ * b) It should run first to draw attention to itself.
+ *
+ * For trues though: this test verifies that all the sysui tests extend the right classes.
+ * This matters because including tests with different context implementations in the same
+ * test suite causes errors, such as the incorrect settings provider being cached.
+ * For an example, see {@link com.android.systemui.DependencyTest}.
+ */
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class AAAPlusPlusVerifySysuiRequiredTestPropertiesTest extends SysuiTestCase {
+
+ private static final String TAG = "AAA++VerifyTest";
+
+ private static final Class[] BASE_CLS_WHITELIST = {
+ SysuiTestCase.class,
+ SysuiBaseFragmentTest.class,
+ };
+
+ private static final Class[] SUPPORTED_SIZES = {
+ SmallTest.class,
+ MediumTest.class,
+ LargeTest.class,
+ android.test.suitebuilder.annotation.SmallTest.class,
+ android.test.suitebuilder.annotation.MediumTest.class,
+ android.test.suitebuilder.annotation.LargeTest.class,
+ };
+
+ @Test
+ public void testAllClassInheritance() throws Throwable {
+ ArrayList<String> fails = new ArrayList<>();
+ for (String className : getClassNamesFromClassPath()) {
+ Class<?> cls = Class.forName(className, false, this.getClass().getClassLoader());
+ if (!isTestClass(cls)) continue;
+
+ boolean hasParent = false;
+ for (Class<?> parent : BASE_CLS_WHITELIST) {
+ if (parent.isAssignableFrom(cls)) {
+ hasParent = true;
+ break;
+ }
+ }
+ boolean hasSize = hasSize(cls);
+ if (!hasSize) {
+ fails.add(cls.getName() + " does not have size annotation, such as @SmallTest");
+ }
+ if (!hasParent) {
+ fails.add(cls.getName() + " does not extend any of " + getClsStr());
+ }
+ }
+
+ assertThat("All sysui test classes must have size and extend one of " + getClsStr(),
+ fails, is(empty()));
+ }
+
+ private boolean hasSize(Class<?> cls) {
+ for (int i = 0; i < SUPPORTED_SIZES.length; i++) {
+ if (cls.getDeclaredAnnotation(SUPPORTED_SIZES[i]) != null) return true;
+ }
+ return false;
+ }
+
+ private Collection<String> getClassNamesFromClassPath() {
+ ClassPathScanner scanner = new ClassPathScanner(mContext.getPackageCodePath());
+
+ ChainedClassNameFilter filter = new ChainedClassNameFilter();
+
+ filter.add(new ExternalClassNameFilter());
+ filter.add(s -> s.startsWith("com.android.systemui")
+ || s.startsWith("com.android.keyguard"));
+
+ try {
+ return scanner.getClassPathEntries(filter);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to scan classes", e);
+ }
+ return Collections.emptyList();
+ }
+
+ private String getClsStr() {
+ return TextUtils.join(",", Arrays.asList(BASE_CLS_WHITELIST)
+ .stream().map(cls -> cls.getSimpleName()).toArray());
+ }
+
+ /**
+ * Determines if given class is a valid test class.
+ *
+ * @return <code>true</code> if loadedClass is a test
+ */
+ private boolean isTestClass(Class<?> loadedClass) {
+ try {
+ if (Modifier.isAbstract(loadedClass.getModifiers())) {
+ logDebug(String.format("Skipping abstract class %s: not a test",
+ loadedClass.getName()));
+ return false;
+ }
+ // TODO: try to find upstream junit calls to replace these checks
+ if (junit.framework.Test.class.isAssignableFrom(loadedClass)) {
+ // ensure that if a TestCase, it has at least one test method otherwise
+ // TestSuite will throw error
+ if (junit.framework.TestCase.class.isAssignableFrom(loadedClass)) {
+ return hasJUnit3TestMethod(loadedClass);
+ }
+ return true;
+ }
+ // TODO: look for a 'suite' method?
+ if (loadedClass.isAnnotationPresent(RunWith.class)) {
+ return true;
+ }
+ for (Method testMethod : loadedClass.getMethods()) {
+ if (testMethod.isAnnotationPresent(Test.class)) {
+ return true;
+ }
+ }
+ logDebug(String.format("Skipping class %s: not a test", loadedClass.getName()));
+ return false;
+ } catch (Exception e) {
+ // Defensively catch exceptions - Will throw runtime exception if it cannot load
+ // methods.
+ // For earlier versions of Android (Pre-ICS), Dalvik might try to initialize a class
+ // during getMethods(), fail to do so, hide the error and throw a NoSuchMethodException.
+ // Since the java.lang.Class.getMethods does not declare such an exception, resort to a
+ // generic catch all.
+ // For ICS+, Dalvik will throw a NoClassDefFoundException.
+ Log.w(TAG, String.format("%s in isTestClass for %s", e.toString(),
+ loadedClass.getName()));
+ return false;
+ } catch (Error e) {
+ // defensively catch Errors too
+ Log.w(TAG, String.format("%s in isTestClass for %s", e.toString(),
+ loadedClass.getName()));
+ return false;
+ }
+ }
+
+ private boolean hasJUnit3TestMethod(Class<?> loadedClass) {
+ for (Method testMethod : loadedClass.getMethods()) {
+ if (isPublicTestMethod(testMethod)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // copied from junit.framework.TestSuite
+ private boolean isPublicTestMethod(Method m) {
+ return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
+ }
+
+ // copied from junit.framework.TestSuite
+ private boolean isTestMethod(Method m) {
+ return m.getParameterTypes().length == 0 && m.getName().startsWith("test")
+ && m.getReturnType().equals(Void.TYPE);
+ }
+
+ /**
+ * Utility method for logging debug messages. Only actually logs a message if TAG is marked
+ * as loggable to limit log spam during normal use.
+ */
+ private void logDebug(String msg) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, msg);
+ }
+ }
+}
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java
new file mode 100644
index 000000000000..901d2006eb12
--- /dev/null
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java
@@ -0,0 +1,408 @@
+/*
+ * 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.navigationbar.car;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableResources;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.statusbar.car.hvac.HvacController;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import dagger.Lazy;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class CarNavigationBarControllerTest extends SysuiTestCase {
+
+ private CarNavigationBarController mCarNavigationBar;
+ private NavigationBarViewFactory mNavigationBarViewFactory;
+ private Lazy<HvacController> mHvacControllerLazy;
+ private TestableResources mTestableResources;
+
+ @Mock
+ private HvacController mHvacController;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mNavigationBarViewFactory = new NavigationBarViewFactory(mContext);
+ mHvacControllerLazy = () -> mHvacController;
+ mTestableResources = mContext.getOrCreateTestableResources();
+
+ // Needed to inflate top navigation bar.
+ mDependency.injectMockDependency(DarkIconDispatcher.class);
+ mDependency.injectMockDependency(StatusBarIconController.class);
+ }
+
+ @Test
+ public void testConnectToHvac_callsConnect() {
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ mCarNavigationBar.connectToHvac();
+
+ verify(mHvacController).connectToCarService();
+ }
+
+ @Test
+ public void testRemoveAllFromHvac_callsRemoveAll() {
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ mCarNavigationBar.removeAllFromHvac();
+
+ verify(mHvacController).removeAllComponents();
+ }
+
+ @Test
+ public void testGetBottomWindow_bottomDisabled_returnsNull() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, false);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getBottomWindow();
+
+ assertThat(window).isNull();
+ }
+
+ @Test
+ public void testGetBottomWindow_bottomEnabled_returnsWindow() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getBottomWindow();
+
+ assertThat(window).isNotNull();
+ }
+
+ @Test
+ public void testGetBottomWindow_bottomEnabled_calledTwice_returnsSameWindow() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window1 = mCarNavigationBar.getBottomWindow();
+ ViewGroup window2 = mCarNavigationBar.getBottomWindow();
+
+ assertThat(window1).isEqualTo(window2);
+ }
+
+ @Test
+ public void testGetLeftWindow_leftDisabled_returnsNull() {
+ mTestableResources.addOverride(R.bool.config_enableLeftNavigationBar, false);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+ ViewGroup window = mCarNavigationBar.getLeftWindow();
+ assertThat(window).isNull();
+ }
+
+ @Test
+ public void testGetLeftWindow_leftEnabled_returnsWindow() {
+ mTestableResources.addOverride(R.bool.config_enableLeftNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getLeftWindow();
+
+ assertThat(window).isNotNull();
+ }
+
+ @Test
+ public void testGetLeftWindow_leftEnabled_calledTwice_returnsSameWindow() {
+ mTestableResources.addOverride(R.bool.config_enableLeftNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window1 = mCarNavigationBar.getLeftWindow();
+ ViewGroup window2 = mCarNavigationBar.getLeftWindow();
+
+ assertThat(window1).isEqualTo(window2);
+ }
+
+ @Test
+ public void testGetRightWindow_rightDisabled_returnsNull() {
+ mTestableResources.addOverride(R.bool.config_enableRightNavigationBar, false);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getRightWindow();
+
+ assertThat(window).isNull();
+ }
+
+ @Test
+ public void testGetRightWindow_rightEnabled_returnsWindow() {
+ mTestableResources.addOverride(R.bool.config_enableRightNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getRightWindow();
+
+ assertThat(window).isNotNull();
+ }
+
+ @Test
+ public void testGetRightWindow_rightEnabled_calledTwice_returnsSameWindow() {
+ mTestableResources.addOverride(R.bool.config_enableRightNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window1 = mCarNavigationBar.getRightWindow();
+ ViewGroup window2 = mCarNavigationBar.getRightWindow();
+
+ assertThat(window1).isEqualTo(window2);
+ }
+
+ @Test
+ public void testSetBottomWindowVisibility_setTrue_isVisible() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getBottomWindow();
+ mCarNavigationBar.setBottomWindowVisibility(View.VISIBLE);
+
+ assertThat(window.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void testSetBottomWindowVisibility_setFalse_isGone() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getBottomWindow();
+ mCarNavigationBar.setBottomWindowVisibility(View.GONE);
+
+ assertThat(window.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void testSetLeftWindowVisibility_setTrue_isVisible() {
+ mTestableResources.addOverride(R.bool.config_enableLeftNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getLeftWindow();
+ mCarNavigationBar.setLeftWindowVisibility(View.VISIBLE);
+
+ assertThat(window.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void testSetLeftWindowVisibility_setFalse_isGone() {
+ mTestableResources.addOverride(R.bool.config_enableLeftNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getLeftWindow();
+ mCarNavigationBar.setLeftWindowVisibility(View.GONE);
+
+ assertThat(window.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void testSetRightWindowVisibility_setTrue_isVisible() {
+ mTestableResources.addOverride(R.bool.config_enableRightNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getRightWindow();
+ mCarNavigationBar.setRightWindowVisibility(View.VISIBLE);
+
+ assertThat(window.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void testSetRightWindowVisibility_setFalse_isGone() {
+ mTestableResources.addOverride(R.bool.config_enableRightNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ ViewGroup window = mCarNavigationBar.getRightWindow();
+ mCarNavigationBar.setRightWindowVisibility(View.GONE);
+
+ assertThat(window.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void testRegisterBottomBarTouchListener_createViewFirst_registrationSuccessful() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ View.OnTouchListener controller = bottomBar.getStatusBarWindowTouchListener();
+ assertThat(controller).isNull();
+ mCarNavigationBar.registerBottomBarTouchListener(mock(View.OnTouchListener.class));
+ controller = bottomBar.getStatusBarWindowTouchListener();
+
+ assertThat(controller).isNotNull();
+ }
+
+ @Test
+ public void testRegisterBottomBarTouchListener_registerFirst_registrationSuccessful() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ mCarNavigationBar.registerBottomBarTouchListener(mock(View.OnTouchListener.class));
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ View.OnTouchListener controller = bottomBar.getStatusBarWindowTouchListener();
+
+ assertThat(controller).isNotNull();
+ }
+
+ @Test
+ public void testRegisterNotificationController_createViewFirst_registrationSuccessful() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ CarNavigationBarController.NotificationsShadeController controller =
+ bottomBar.getNotificationsPanelController();
+ assertThat(controller).isNull();
+ mCarNavigationBar.registerNotificationController(
+ mock(CarNavigationBarController.NotificationsShadeController.class));
+ controller = bottomBar.getNotificationsPanelController();
+
+ assertThat(controller).isNotNull();
+ }
+
+ @Test
+ public void testRegisterNotificationController_registerFirst_registrationSuccessful() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+
+ mCarNavigationBar.registerNotificationController(
+ mock(CarNavigationBarController.NotificationsShadeController.class));
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ CarNavigationBarController.NotificationsShadeController controller =
+ bottomBar.getNotificationsPanelController();
+
+ assertThat(controller).isNotNull();
+ }
+
+ @Test
+ public void testShowAllKeyguardButtons_bottomEnabled_bottomKeyguardButtonsVisible() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ View bottomKeyguardButtons = bottomBar.findViewById(R.id.lock_screen_nav_buttons);
+
+ mCarNavigationBar.showAllKeyguardButtons(/* isSetUp= */ true);
+
+ assertThat(bottomKeyguardButtons.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void testShowAllKeyguardButtons_bottomEnabled_bottomNavButtonsGone() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ View bottomButtons = bottomBar.findViewById(R.id.nav_buttons);
+
+ mCarNavigationBar.showAllKeyguardButtons(/* isSetUp= */ true);
+
+ assertThat(bottomButtons.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void testHideAllKeyguardButtons_bottomEnabled_bottomKeyguardButtonsGone() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ View bottomKeyguardButtons = bottomBar.findViewById(R.id.lock_screen_nav_buttons);
+
+ mCarNavigationBar.showAllKeyguardButtons(/* isSetUp= */ true);
+ assertThat(bottomKeyguardButtons.getVisibility()).isEqualTo(View.VISIBLE);
+ mCarNavigationBar.hideAllKeyguardButtons(/* isSetUp= */ true);
+
+ assertThat(bottomKeyguardButtons.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void testHideAllKeyguardButtons_bottomEnabled_bottomNavButtonsVisible() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ View bottomButtons = bottomBar.findViewById(R.id.nav_buttons);
+
+ mCarNavigationBar.showAllKeyguardButtons(/* isSetUp= */ true);
+ assertThat(bottomButtons.getVisibility()).isEqualTo(View.GONE);
+ mCarNavigationBar.hideAllKeyguardButtons(/* isSetUp= */ true);
+
+ assertThat(bottomButtons.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void testToggleAllNotificationsUnseenIndicator_bottomEnabled_hasUnseen_setCorrectly() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ CarNavigationButton notifications = bottomBar.findViewById(R.id.notifications);
+
+ boolean hasUnseen = true;
+ mCarNavigationBar.toggleAllNotificationsUnseenIndicator(/* isSetUp= */ true,
+ hasUnseen);
+
+ assertThat(notifications.getUnseen()).isTrue();
+ }
+
+ @Test
+ public void testToggleAllNotificationsUnseenIndicator_bottomEnabled_noUnseen_setCorrectly() {
+ mTestableResources.addOverride(R.bool.config_enableBottomNavigationBar, true);
+ mCarNavigationBar = new CarNavigationBarController(mContext, mNavigationBarViewFactory,
+ mHvacControllerLazy);
+ CarNavigationBarView bottomBar = mCarNavigationBar.getBottomBar(/* isSetUp= */ true);
+ CarNavigationButton notifications = bottomBar.findViewById(R.id.notifications);
+
+ boolean hasUnseen = false;
+ mCarNavigationBar.toggleAllNotificationsUnseenIndicator(/* isSetUp= */ true,
+ hasUnseen);
+
+ assertThat(notifications.getUnseen()).isFalse();
+ }
+}
diff --git a/packages/SettingsLib/res/values-af/strings.xml b/packages/SettingsLib/res/values-af/strings.xml
index 245ca140b4f0..f25b5eb294e0 100644
--- a/packages/SettingsLib/res/values-af/strings.xml
+++ b/packages/SettingsLib/res/values-af/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Beskikbaar via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Tik om aan te meld"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Gekoppel, geen internet nie"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Daar kan nie by private DNS-bediener ingegaan word nie"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Beperkte verbinding"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Geen internet nie"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Aanmelding word vereis"</string>
diff --git a/packages/SettingsLib/res/values-am/strings.xml b/packages/SettingsLib/res/values-am/strings.xml
index bfd31968bd6c..6332c848c5a6 100644
--- a/packages/SettingsLib/res/values-am/strings.xml
+++ b/packages/SettingsLib/res/values-am/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"በ%1$s በኩል የሚገኝ"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"ለመመዝገብ መታ ያድርጉ"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"ተገናኝቷል፣ ምንም በይነመረብ የለም"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"የግል ዲኤንኤስ አገልጋይ ሊደረስበት አይችልም"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"የተገደበ ግንኙነት"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ምንም በይነመረብ የለም"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"ወደ መለያ መግባት ያስፈልጋል"</string>
diff --git a/packages/SettingsLib/res/values-ar/strings.xml b/packages/SettingsLib/res/values-ar/strings.xml
index 8b67b0b1347e..8c72527604a4 100644
--- a/packages/SettingsLib/res/values-ar/strings.xml
+++ b/packages/SettingsLib/res/values-ar/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"‏متوفرة عبر %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"انقر للاشتراك."</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"متصلة ولكن بلا إنترنت"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"لا يمكن الوصول إلى خادم أسماء نظام نطاقات خاص"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"اتصال محدود"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"لا يتوفر اتصال إنترنت."</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"يلزم تسجيل الدخول"</string>
diff --git a/packages/SettingsLib/res/values-as/strings.xml b/packages/SettingsLib/res/values-as/strings.xml
index f3ca337b261d..a7ea1e020f03 100644
--- a/packages/SettingsLib/res/values-as/strings.xml
+++ b/packages/SettingsLib/res/values-as/strings.xml
@@ -43,6 +43,8 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$sৰ মাধ্যমেৰে উপলব্ধ"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"ছাইন আপ কৰিবলৈ টিপক"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"সংযোজিত, ইণ্টাৰনেট নাই"</string>
+ <!-- no translation found for private_dns_broken (7356676011023412490) -->
+ <skip />
<string name="wifi_limited_connection" msgid="7717855024753201527">"ইণ্টাৰনেট সংযোগ সীমিত"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ইণ্টাৰনেট সংযোগ নাই"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"ছাইন ইন কৰা দৰকাৰী"</string>
diff --git a/packages/SettingsLib/res/values-az/strings.xml b/packages/SettingsLib/res/values-az/strings.xml
index d35dfe8255d2..cb7db78fd018 100644
--- a/packages/SettingsLib/res/values-az/strings.xml
+++ b/packages/SettingsLib/res/values-az/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s vasitəsilə əlçatandır"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Qeydiyyatdan keçmək üçün klikləyin"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Qoşuludur, internet yoxdur"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Özəl DNS serverinə giriş mümkün deyil"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Məhdud bağlantı"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"İnternet yoxdur"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Giriş tələb olunur"</string>
diff --git a/packages/SettingsLib/res/values-b+sr+Latn/strings.xml b/packages/SettingsLib/res/values-b+sr+Latn/strings.xml
index 448de4b8b7dd..75feb326cd77 100644
--- a/packages/SettingsLib/res/values-b+sr+Latn/strings.xml
+++ b/packages/SettingsLib/res/values-b+sr+Latn/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Dostupna je preko pristupne tačke %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Dodirnite da biste se registrovali"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Veza je uspostavljena, nema interneta"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Pristup privatnom DNS serveru nije uspeo"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ograničena veza"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Nema interneta"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Treba da se prijavite"</string>
diff --git a/packages/SettingsLib/res/values-be/strings.xml b/packages/SettingsLib/res/values-be/strings.xml
index d68c0f3e02ab..677aa24ac43d 100644
--- a/packages/SettingsLib/res/values-be/strings.xml
+++ b/packages/SettingsLib/res/values-be/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Даступна праз %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Націсніце, каб зарэгістравацца"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Падключана, без доступу да інтэрнэту"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Не ўдалося атрымаць доступ да прыватнага DNS-сервера"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Абмежаваныя магчымасці падключэння"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Не падключана да інтэрнэту"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Трэба выканаць уваход"</string>
diff --git a/packages/SettingsLib/res/values-bg/strings.xml b/packages/SettingsLib/res/values-bg/strings.xml
index cb99f64ba498..162042209c46 100644
--- a/packages/SettingsLib/res/values-bg/strings.xml
+++ b/packages/SettingsLib/res/values-bg/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Мрежата е достъпна през „%1$s“"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Докоснете, за да се регистрирате"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Установена е връзка – няма достъп до интернет"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Не може да се осъществи достъп до частния DNS сървър"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ограничена връзка"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Няма връзка с интернет"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Изисква се вход в профила"</string>
diff --git a/packages/SettingsLib/res/values-bn/strings.xml b/packages/SettingsLib/res/values-bn/strings.xml
index f2f4f5249b74..b1e37a66fa82 100644
--- a/packages/SettingsLib/res/values-bn/strings.xml
+++ b/packages/SettingsLib/res/values-bn/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s এর মাধ্যমে উপলব্ধ"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"সাইন-আপ করতে ট্যাপ করুন"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"কানেক্ট, ইন্টারনেট নেই"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"ব্যক্তিগত ডিএনএস সার্ভার অ্যাক্সেস করা যাবে না"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"সীমিত কানেকশন"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ইন্টারনেট কানেকশন নেই"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"সাইন-ইন করা দরকার"</string>
diff --git a/packages/SettingsLib/res/values-bs/strings.xml b/packages/SettingsLib/res/values-bs/strings.xml
index 45b8dd99ec70..911a8315523f 100644
--- a/packages/SettingsLib/res/values-bs/strings.xml
+++ b/packages/SettingsLib/res/values-bs/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Dostupan preko %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Dodirnite za prijavu"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Povezano, nema interneta"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Nije moguće pristupiti privatnom DNS serveru"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ograničena veza"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Nema internetske veze"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Potrebna je prijava"</string>
diff --git a/packages/SettingsLib/res/values-ca/strings.xml b/packages/SettingsLib/res/values-ca/strings.xml
index b624df056bec..58c2b670630d 100644
--- a/packages/SettingsLib/res/values-ca/strings.xml
+++ b/packages/SettingsLib/res/values-ca/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Disponible mitjançant %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Toca per registrar-te"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Connectada, sense Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"No es pot accedir al servidor DNS privat"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Connexió limitada"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Sense connexió a Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Cal iniciar la sessió"</string>
@@ -237,7 +238,7 @@
<string name="bluetooth_select_a2dp_codec_channel_mode_dialog_title" msgid="7234956835280563341">"Activa el còdec d\'àudio per Bluetooth\nSelecció: mode de canal"</string>
<string name="bluetooth_select_a2dp_codec_ldac_playback_quality" msgid="3619694372407843405">"Còdec LDAC d\'àudio per Bluetooth: qualitat de reproducció"</string>
<string name="bluetooth_select_a2dp_codec_ldac_playback_quality_dialog_title" msgid="6893955536658137179">"Activa l\'LDAC d\'àudio per Bluetooth\nSelecció de còdec: qualitat de reproducció"</string>
- <string name="bluetooth_select_a2dp_codec_streaming_label" msgid="5347862512596240506">"S\'està reproduint en temps real: <xliff:g id="STREAMING_PARAMETER">%1$s</xliff:g>"</string>
+ <string name="bluetooth_select_a2dp_codec_streaming_label" msgid="5347862512596240506">"Reproducció en continu: <xliff:g id="STREAMING_PARAMETER">%1$s</xliff:g>"</string>
<string name="select_private_dns_configuration_title" msgid="3700456559305263922">"DNS privat"</string>
<string name="select_private_dns_configuration_dialog_title" msgid="9221994415765826811">"Selecciona el mode de DNS privat"</string>
<string name="private_dns_mode_off" msgid="8236575187318721684">"Desactivat"</string>
diff --git a/packages/SettingsLib/res/values-cs/strings.xml b/packages/SettingsLib/res/values-cs/strings.xml
index 042e12a14967..3c3d5b811d26 100644
--- a/packages/SettingsLib/res/values-cs/strings.xml
+++ b/packages/SettingsLib/res/values-cs/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Dostupné prostřednictvím %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Klepnutím se zaregistrujete"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Připojeno, není k dispozici internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Nelze získat přístup k soukromému serveru DNS"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Omezené připojení"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Nejste připojeni k internetu"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Je vyžadováno přihlášení"</string>
diff --git a/packages/SettingsLib/res/values-da/strings.xml b/packages/SettingsLib/res/values-da/strings.xml
index 4723293f302d..bf5d6cfd472d 100644
--- a/packages/SettingsLib/res/values-da/strings.xml
+++ b/packages/SettingsLib/res/values-da/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Tilgængelig via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Tryk for at registrere dig"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Tilsluttet – intet internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Der er ikke adgang til den private DNS-server"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Begrænset forbindelse"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Intet internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Login er påkrævet"</string>
diff --git a/packages/SettingsLib/res/values-de/strings.xml b/packages/SettingsLib/res/values-de/strings.xml
index 4f5a965f9ce0..a6dbd5ae215a 100644
--- a/packages/SettingsLib/res/values-de/strings.xml
+++ b/packages/SettingsLib/res/values-de/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Verfügbar über %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Zum Anmelden tippen"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Verbunden, kein Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Auf den privaten DNS-Server kann nicht zugegriffen werden"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Eingeschränkte Verbindung"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Kein Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Anmeldung erforderlich"</string>
diff --git a/packages/SettingsLib/res/values-el/strings.xml b/packages/SettingsLib/res/values-el/strings.xml
index 753dea8855dd..fdba74a4dfd8 100644
--- a/packages/SettingsLib/res/values-el/strings.xml
+++ b/packages/SettingsLib/res/values-el/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Διαθέσιμο μέσω %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Πατήστε για εγγραφή"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Συνδέθηκε, χωρίς σύνδεση στο διαδίκτυο"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Δεν είναι δυνατή η πρόσβαση στον ιδιωτικό διακομιστή DNS."</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Περιορισμένη σύνδεση"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Δεν υπάρχει σύνδεση στο διαδίκτυο"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Απαιτείται σύνδεση"</string>
diff --git a/packages/SettingsLib/res/values-en-rAU/strings.xml b/packages/SettingsLib/res/values-en-rAU/strings.xml
index dd3d27803db3..581adf86d062 100644
--- a/packages/SettingsLib/res/values-en-rAU/strings.xml
+++ b/packages/SettingsLib/res/values-en-rAU/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Available via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Tap to sign up"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Connected, no Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Private DNS server cannot be accessed"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Limited connection"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"No Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Sign-in required"</string>
diff --git a/packages/SettingsLib/res/values-en-rCA/strings.xml b/packages/SettingsLib/res/values-en-rCA/strings.xml
index dd3d27803db3..581adf86d062 100644
--- a/packages/SettingsLib/res/values-en-rCA/strings.xml
+++ b/packages/SettingsLib/res/values-en-rCA/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Available via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Tap to sign up"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Connected, no Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Private DNS server cannot be accessed"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Limited connection"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"No Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Sign-in required"</string>
diff --git a/packages/SettingsLib/res/values-en-rGB/strings.xml b/packages/SettingsLib/res/values-en-rGB/strings.xml
index dd3d27803db3..581adf86d062 100644
--- a/packages/SettingsLib/res/values-en-rGB/strings.xml
+++ b/packages/SettingsLib/res/values-en-rGB/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Available via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Tap to sign up"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Connected, no Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Private DNS server cannot be accessed"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Limited connection"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"No Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Sign-in required"</string>
diff --git a/packages/SettingsLib/res/values-en-rIN/strings.xml b/packages/SettingsLib/res/values-en-rIN/strings.xml
index dd3d27803db3..581adf86d062 100644
--- a/packages/SettingsLib/res/values-en-rIN/strings.xml
+++ b/packages/SettingsLib/res/values-en-rIN/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Available via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Tap to sign up"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Connected, no Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Private DNS server cannot be accessed"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Limited connection"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"No Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Sign-in required"</string>
diff --git a/packages/SettingsLib/res/values-en-rXC/strings.xml b/packages/SettingsLib/res/values-en-rXC/strings.xml
index d9f61d8188f1..e75d7bc19bbf 100644
--- a/packages/SettingsLib/res/values-en-rXC/strings.xml
+++ b/packages/SettingsLib/res/values-en-rXC/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎‏‏‎‏‏‎‎‏‏‏‎‎‏‎‎‏‎‎‏‏‎‏‏‏‏‏‏‎‎‎‏‎‏‏‏‎‎‏‏‏‏‏‎‎‏‎‎‏‏‎‏‎‏‏‎‎‎‎‏‏‎‏‎Available via %1$s‎‏‎‎‏‎"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‎‎‎‎‏‎‎‎‎‎‎‏‏‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‎‏‏‎‎‏‎‏‎‎‎‎‏‏‎‏‎‎‏‏‎‏‎‏‎‎Tap to sign up‎‏‎‎‏‎"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‎‎‎‏‏‏‎‏‎‏‏‎‏‎‎‏‎‎‏‏‏‏‎‏‏‎‎‏‎‏‎‎‏‏‏‎‏‎‎‏‏‎‎‏‎‎‎‏‎‎‎‏‏‎‏‎‎‏‎‏‎Connected, no internet‎‏‎‎‏‎"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‏‏‎‎‎‎‏‏‎‎‎‎‎‏‎‏‎‎‏‏‏‎‏‏‎‏‎‏‏‎‎‏‏‎‎‏‎‏‏‎‎‎‎‎‎‏‎‏‏‎‏‎‎‎‎‏‎‏‎‎Private DNS server cannot be accessed‎‏‎‎‏‎"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‎‏‏‎‏‎‏‎‏‎‎‎‏‎‎‎‎‎‏‎‎‎‎‏‎‏‎‎‏‏‎‎‎‎‎‏‏‎‏‎‎‎‏‎‏‏‏‎‏‏‏‎Limited connection‎‏‎‎‏‎"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‎‎‎‏‏‏‎‏‏‎‏‎‏‏‏‏‏‏‏‎‏‏‏‎‎‏‎‏‎‏‎‏‏‏‏‏‎‎‎‏‎‏‎‏‎‏‏‎‏‎‏‎‎‏‎No internet‎‏‎‎‏‎"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‎‎‏‏‏‎‏‏‎‏‏‎‏‏‎‏‎‎‏‎‎‎‏‏‏‎‎‎‏‎‎‏‏‏‎‏‏‎‏‎‏‏‏‎‏‏‏‎‎‎‎‏‎‏‏‏‎‏‎‎‎Sign in required‎‏‎‎‏‎"</string>
diff --git a/packages/SettingsLib/res/values-es-rUS/strings.xml b/packages/SettingsLib/res/values-es-rUS/strings.xml
index 30cb0a1f113f..97cce55c11d8 100644
--- a/packages/SettingsLib/res/values-es-rUS/strings.xml
+++ b/packages/SettingsLib/res/values-es-rUS/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Disponible a través de %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Presiona para registrarte"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Conectado pero sin conexión a Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"No se puede acceder al servidor DNS privado"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Conexión limitada"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Sin Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Acceso obligatorio"</string>
diff --git a/packages/SettingsLib/res/values-es/strings.xml b/packages/SettingsLib/res/values-es/strings.xml
index 32905dfb9213..7ba1a9435be2 100644
--- a/packages/SettingsLib/res/values-es/strings.xml
+++ b/packages/SettingsLib/res/values-es/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Disponible a través de %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Toca para registrarte"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Conexión sin Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"No se ha podido acceder al servidor DNS privado"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Conexión limitada"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Sin Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Debes iniciar sesión"</string>
diff --git a/packages/SettingsLib/res/values-et/strings.xml b/packages/SettingsLib/res/values-et/strings.xml
index 79b8a848f870..0e987528d73a 100644
--- a/packages/SettingsLib/res/values-et/strings.xml
+++ b/packages/SettingsLib/res/values-et/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Saadaval üksuse %1$s kaudu"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Puudutage registreerumiseks"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Ühendatud, Interneti-ühendus puudub"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Privaatsele DNS-serverile ei pääse juurde"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Piiratud ühendus"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Interneti-ühendus puudub"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Nõutav on sisselogimine"</string>
diff --git a/packages/SettingsLib/res/values-eu/strings.xml b/packages/SettingsLib/res/values-eu/strings.xml
index 38ae9c243f6e..872e9a56ed87 100644
--- a/packages/SettingsLib/res/values-eu/strings.xml
+++ b/packages/SettingsLib/res/values-eu/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s bidez erabilgarri"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Sakatu erregistratzeko"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Konektatuta; ezin da atzitu Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Ezin da atzitu DNS zerbitzari pribatua"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Konexio mugatua"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Ez dago Interneteko konexiorik"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Saioa hasi behar da"</string>
diff --git a/packages/SettingsLib/res/values-fa/strings.xml b/packages/SettingsLib/res/values-fa/strings.xml
index a883d6cddf47..6e281fe05486 100644
--- a/packages/SettingsLib/res/values-fa/strings.xml
+++ b/packages/SettingsLib/res/values-fa/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"‏در دسترس از طریق %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"برای ثبت‌نام ضربه بزنید"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"متصل، بدون اینترنت"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"‏سرور DNS خصوصی قابل دسترسی نیست"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"اتصال محدود"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"عدم دسترسی به اینترنت"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"ورود به سیستم لازم است"</string>
diff --git a/packages/SettingsLib/res/values-fi/strings.xml b/packages/SettingsLib/res/values-fi/strings.xml
index 929e101bf610..8c3630a08c9b 100644
--- a/packages/SettingsLib/res/values-fi/strings.xml
+++ b/packages/SettingsLib/res/values-fi/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Käytettävissä seuraavan kautta: %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Rekisteröidy napauttamalla"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Yhdistetty, ei internetyhteyttä"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Ei pääsyä yksityiselle DNS-palvelimelle"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Rajallinen yhteys"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Ei internetyhteyttä"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Sisäänkirjautuminen vaaditaan"</string>
diff --git a/packages/SettingsLib/res/values-fr-rCA/strings.xml b/packages/SettingsLib/res/values-fr-rCA/strings.xml
index d3bfc96ea473..6c5834ae793a 100644
--- a/packages/SettingsLib/res/values-fr-rCA/strings.xml
+++ b/packages/SettingsLib/res/values-fr-rCA/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Accessible par %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Toucher pour vous connecter"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Connecté, aucun accès à Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Impossible d\'accéder au serveur DNS privé"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Connexion limitée"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Aucune connexion Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Connexion requise"</string>
diff --git a/packages/SettingsLib/res/values-fr/strings.xml b/packages/SettingsLib/res/values-fr/strings.xml
index b5706cea932d..508556ece88a 100644
--- a/packages/SettingsLib/res/values-fr/strings.xml
+++ b/packages/SettingsLib/res/values-fr/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Disponible via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Appuyez ici pour vous connecter"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Connecté, aucun accès à Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Impossible d\'accéder au serveur DNS privé"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Connexion limitée"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Aucun accès à Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Connexion requise"</string>
diff --git a/packages/SettingsLib/res/values-gl/strings.xml b/packages/SettingsLib/res/values-gl/strings.xml
index 2aa1604cca21..1a3ae3d601e1 100644
--- a/packages/SettingsLib/res/values-gl/strings.xml
+++ b/packages/SettingsLib/res/values-gl/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Dispoñible a través de %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Toca para rexistrarte"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Conexión sen Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Non se puido acceder ao servidor DNS privado"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Pouca conexión"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Non hai conexión a Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"É obrigatorio iniciar sesión"</string>
diff --git a/packages/SettingsLib/res/values-gu/strings.xml b/packages/SettingsLib/res/values-gu/strings.xml
index c2cc4c5d9410..b3f518541c71 100644
--- a/packages/SettingsLib/res/values-gu/strings.xml
+++ b/packages/SettingsLib/res/values-gu/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s દ્વારા ઉપલબ્ધ"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"સાઇન અપ કરવા માટે ટૅપ કરો"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"કનેક્ટ કર્યું, કોઈ ઇન્ટરનેટ નથી"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"ખાનગી DNS સર્વર ઍક્સેસ કરી શકાતા નથી"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"મર્યાદિત કનેક્શન"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ઇન્ટરનેટ ઍક્સેસ નથી"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"સાઇન ઇન આવશ્યક"</string>
diff --git a/packages/SettingsLib/res/values-hi/strings.xml b/packages/SettingsLib/res/values-hi/strings.xml
index 66c7a16e1bf6..454773ca3538 100644
--- a/packages/SettingsLib/res/values-hi/strings.xml
+++ b/packages/SettingsLib/res/values-hi/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s के द्वारा उपलब्ध"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"साइन अप करने के लिए टैप करें"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"कनेक्ट हो गया है, लेकिन इंटरनेट नहीं है"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"निजी डीएनएस सर्वर को ऐक्सेस नहीं किया जा सकता"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"सीमित कनेक्शन"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"इंटरनेट कनेक्शन नहीं है"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"साइन इन करना ज़रूरी है"</string>
diff --git a/packages/SettingsLib/res/values-hr/strings.xml b/packages/SettingsLib/res/values-hr/strings.xml
index 0fc16f847822..0ec154baa0e9 100644
--- a/packages/SettingsLib/res/values-hr/strings.xml
+++ b/packages/SettingsLib/res/values-hr/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Dostupno putem %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Dodirnite da biste se registrirali"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Povezano, bez interneta"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Nije moguće pristupiti privatnom DNS poslužitelju"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ograničena veza"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Nema interneta"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Obavezna prijava"</string>
diff --git a/packages/SettingsLib/res/values-hu/strings.xml b/packages/SettingsLib/res/values-hu/strings.xml
index 1388dbb684fa..4a0af7d27ab5 100644
--- a/packages/SettingsLib/res/values-hu/strings.xml
+++ b/packages/SettingsLib/res/values-hu/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Elérhető a következőn keresztül: %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Koppintson a regisztrációhoz"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Csatlakozva, nincs internet-hozzáférés"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"A privát DNS-kiszolgálóhoz nem lehet hozzáférni"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Korlátozott kapcsolat"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Nincs internetkapcsolat"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Bejelentkezést igényel"</string>
diff --git a/packages/SettingsLib/res/values-hy/strings.xml b/packages/SettingsLib/res/values-hy/strings.xml
index 2145adb1036e..cc0ecb8125b6 100644
--- a/packages/SettingsLib/res/values-hy/strings.xml
+++ b/packages/SettingsLib/res/values-hy/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Հասանելի է %1$s-ի միջոցով"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Հպեք՝ գրանցվելու համար"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Միացված է, սակայն ինտերնետ կապ չկա"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Մասնավոր DNS սերվերն անհասանելի է"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Սահմանափակ կապ"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Ինտերնետ կապ չկա"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Անհրաժեշտ է մուտք գործել"</string>
diff --git a/packages/SettingsLib/res/values-in/strings.xml b/packages/SettingsLib/res/values-in/strings.xml
index 7ebe6b7f8180..0733c1f76a66 100644
--- a/packages/SettingsLib/res/values-in/strings.xml
+++ b/packages/SettingsLib/res/values-in/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Tersedia melalui %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Ketuk untuk mendaftar"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Tersambung, tidak ada internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Server DNS pribadi tidak dapat diakses"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Koneksi terbatas"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Tidak ada internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Perlu login"</string>
diff --git a/packages/SettingsLib/res/values-is/strings.xml b/packages/SettingsLib/res/values-is/strings.xml
index eede11756e9e..28ed8fc6224a 100644
--- a/packages/SettingsLib/res/values-is/strings.xml
+++ b/packages/SettingsLib/res/values-is/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Í boði í gegnum %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Ýttu til að skrá þig"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Tengt, enginn netaðgangur"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Ekki næst í DNS-einkaþjón"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Takmörkuð tenging"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Engin nettenging"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Innskráningar krafist"</string>
diff --git a/packages/SettingsLib/res/values-it/strings.xml b/packages/SettingsLib/res/values-it/strings.xml
index 2c64ec2e6705..b0569b0f96f2 100644
--- a/packages/SettingsLib/res/values-it/strings.xml
+++ b/packages/SettingsLib/res/values-it/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Disponibile tramite %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Tocca per registrarti"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Connesso, senza Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Non è possibile accedere al server DNS privato"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Connessione limitata"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Nessuna connessione a Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Accesso richiesto"</string>
diff --git a/packages/SettingsLib/res/values-iw/strings.xml b/packages/SettingsLib/res/values-iw/strings.xml
index 30d6a4aa1d5e..f7d4fcd317ad 100644
--- a/packages/SettingsLib/res/values-iw/strings.xml
+++ b/packages/SettingsLib/res/values-iw/strings.xml
@@ -43,6 +43,8 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"‏זמינה דרך %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"יש להקיש כדי להירשם"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"מחובר. אין אינטרנט"</string>
+ <!-- no translation found for private_dns_broken (7356676011023412490) -->
+ <skip />
<string name="wifi_limited_connection" msgid="7717855024753201527">"חיבור מוגבל"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"אין אינטרנט"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"נדרשת כניסה"</string>
diff --git a/packages/SettingsLib/res/values-ja/strings.xml b/packages/SettingsLib/res/values-ja/strings.xml
index 6cb5514cc582..4d5c86cafe3a 100644
--- a/packages/SettingsLib/res/values-ja/strings.xml
+++ b/packages/SettingsLib/res/values-ja/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s経由で使用可能"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"タップして登録してください"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"接続済み、インターネット接続なし"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"プライベート DNS サーバーにアクセスできません"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"接続が制限されています"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"インターネット未接続"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"ログインが必要"</string>
diff --git a/packages/SettingsLib/res/values-ka/strings.xml b/packages/SettingsLib/res/values-ka/strings.xml
index 4c78a3864562..20342bc011cd 100644
--- a/packages/SettingsLib/res/values-ka/strings.xml
+++ b/packages/SettingsLib/res/values-ka/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"ხელმისაწვდომია %1$s-ით"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"შეეხეთ რეგისტრაციისთვის"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"დაკავშირებულია, ინტერნეტის გარეშე"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"პირად DNS სერვერზე წვდომა შეუძლებელია"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"შეზღუდული კავშირი"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ინტერნეტ-კავშირი არ არის"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"აუცილებელია სისტემაში შესვლა"</string>
diff --git a/packages/SettingsLib/res/values-kk/strings.xml b/packages/SettingsLib/res/values-kk/strings.xml
index ce086577ec00..7b6d29eb43ad 100644
--- a/packages/SettingsLib/res/values-kk/strings.xml
+++ b/packages/SettingsLib/res/values-kk/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s арқылы қолжетімді"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Тіркелу үшін түртіңіз."</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Қосылған, интернет жоқ"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Жеке DNS серверіне кіру мүмкін емес."</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Шектеулі байланыс"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Интернетпен байланыс жоқ"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Есептік жазбаға кіру керек"</string>
diff --git a/packages/SettingsLib/res/values-km/strings.xml b/packages/SettingsLib/res/values-km/strings.xml
index 43c92824015b..115be8e07f59 100644
--- a/packages/SettingsLib/res/values-km/strings.xml
+++ b/packages/SettingsLib/res/values-km/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"មានតាមរយៈ %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"ចុច​ដើម្បី​ចុះឈ្មោះ"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"បាន​ភ្ជាប់ ប៉ុន្តែ​គ្មាន​អ៊ីនធឺណិត​ទេ"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"មិនអាច​ចូលប្រើ​ម៉ាស៊ីនមេ DNS ឯកជន​បានទេ"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"ការតភ្ជាប់មានកម្រិត"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"គ្មាន​អ៊ីនធឺណិតទេ"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"តម្រូវ​ឱ្យ​ចូល​គណនី"</string>
diff --git a/packages/SettingsLib/res/values-kn/strings.xml b/packages/SettingsLib/res/values-kn/strings.xml
index 253104b38661..ac8bfb1654cb 100644
--- a/packages/SettingsLib/res/values-kn/strings.xml
+++ b/packages/SettingsLib/res/values-kn/strings.xml
@@ -43,6 +43,8 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s ಮೂಲಕ ಲಭ್ಯವಿದೆ"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"ಸೈನ್ ಅಪ್ ಮಾಡಲು ಟ್ಯಾಪ್‌ ಮಾಡಿ"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"ಸಂಪರ್ಕಪಡಿಸಲಾಗಿದೆ, ಇಂಟರ್ನೆಟ್ ಇಲ್ಲ"</string>
+ <!-- no translation found for private_dns_broken (7356676011023412490) -->
+ <skip />
<string name="wifi_limited_connection" msgid="7717855024753201527">"ಸೀಮಿತ ಸಂಪರ್ಕ"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ಇಂಟರ್ನೆಟ್ ಇಲ್ಲ"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"ಸೈನ್ ಇನ್ ಮಾಡುವ ಅಗತ್ಯವಿದೆ"</string>
diff --git a/packages/SettingsLib/res/values-ko/strings.xml b/packages/SettingsLib/res/values-ko/strings.xml
index ac44c0db62a3..4e543103fe60 100644
--- a/packages/SettingsLib/res/values-ko/strings.xml
+++ b/packages/SettingsLib/res/values-ko/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s을(를) 통해 사용 가능"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"탭하여 가입"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"연결됨, 인터넷 사용 불가"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"비공개 DNS 서버에 액세스할 수 없습니다."</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"제한된 연결"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"인터넷 연결 없음"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"로그인 필요"</string>
diff --git a/packages/SettingsLib/res/values-ky/strings.xml b/packages/SettingsLib/res/values-ky/strings.xml
index dd1ff30faf44..e891b5a902cb 100644
--- a/packages/SettingsLib/res/values-ky/strings.xml
+++ b/packages/SettingsLib/res/values-ky/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s аркылуу жеткиликтүү"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Катталуу үчүн таптап коюңуз"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Туташып турат, Интернет жок"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Жеке DNS сервери жеткиликсиз"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Байланыш чектелген"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Интернет жок"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Аккаунтка кирүү талап кылынат"</string>
diff --git a/packages/SettingsLib/res/values-lo/strings.xml b/packages/SettingsLib/res/values-lo/strings.xml
index 28e811153ea1..406a42b2b041 100644
--- a/packages/SettingsLib/res/values-lo/strings.xml
+++ b/packages/SettingsLib/res/values-lo/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"ມີ​ໃຫ້​ຜ່ານ %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"ແຕະເພື່ອສະໝັກ"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"ເຊື່ອມຕໍ່ແລ້ວ, ບໍ່ມີອິນເຕີເນັດ"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"ບໍ່ສາມາດເຂົ້າເຖິງເຊີບເວີ DNS ສ່ວນຕົວໄດ້"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"ການເຊື່ອມຕໍ່ຈຳກັດ"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ບໍ່ມີອິນເຕີເນັດ"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"ຈຳເປັນຕ້ອງເຂົ້າສູ່ລະບົບ"</string>
diff --git a/packages/SettingsLib/res/values-lt/strings.xml b/packages/SettingsLib/res/values-lt/strings.xml
index 93fcaa798998..b305fd90d1cc 100644
--- a/packages/SettingsLib/res/values-lt/strings.xml
+++ b/packages/SettingsLib/res/values-lt/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Pasiekiama naudojant „%1$s“"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Palieskite, kad prisiregistruotumėte"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Prisijungta, nėra interneto"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Privataus DNS serverio negalima pasiekti"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ribotas ryšys"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Nėra interneto ryšio"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Reikia prisijungti"</string>
diff --git a/packages/SettingsLib/res/values-lv/strings.xml b/packages/SettingsLib/res/values-lv/strings.xml
index c12d097232f4..c9e2c1114556 100644
--- a/packages/SettingsLib/res/values-lv/strings.xml
+++ b/packages/SettingsLib/res/values-lv/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Pieejams, izmantojot %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Pieskarieties, lai reģistrētos"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Savienojums izveidots, nav piekļuves internetam"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Nevar piekļūt privātam DNS serverim."</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ierobežots savienojums"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Nav piekļuves internetam"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Nepieciešama pierakstīšanās"</string>
diff --git a/packages/SettingsLib/res/values-mk/strings.xml b/packages/SettingsLib/res/values-mk/strings.xml
index 9a76a0a8e04c..e06d414c2453 100644
--- a/packages/SettingsLib/res/values-mk/strings.xml
+++ b/packages/SettingsLib/res/values-mk/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Достапно преку %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Допрете за да се регистрирате"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Поврзана, нема интернет"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Не може да се пристапи до приватниот DNS-сервер"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ограничена врска"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Нема интернет"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Потребно е најавување"</string>
diff --git a/packages/SettingsLib/res/values-ml/strings.xml b/packages/SettingsLib/res/values-ml/strings.xml
index bb762951d437..36e632a917f5 100644
--- a/packages/SettingsLib/res/values-ml/strings.xml
+++ b/packages/SettingsLib/res/values-ml/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s വഴി ലഭ്യം"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"സൈൻ അപ്പ് ചെയ്യാൻ ടാപ്പ് ചെയ്യുക"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"കണക്റ്റ് ചെയ്‌തു, ഇന്റർനെറ്റ് ഇല്ല"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"സ്വകാര്യ DNS സെർവർ ആക്‌സസ് ചെയ്യാനാവില്ല"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"പരിമിത കണക്‌ഷൻ"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ഇന്റർനെറ്റ് ഇല്ല"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"സൈൻ ഇൻ ചെയ്യേണ്ടത് ആവശ്യമാണ്"</string>
diff --git a/packages/SettingsLib/res/values-mn/strings.xml b/packages/SettingsLib/res/values-mn/strings.xml
index 65a8ca6c1253..1a5a0af7bd5c 100644
--- a/packages/SettingsLib/res/values-mn/strings.xml
+++ b/packages/SettingsLib/res/values-mn/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s-р боломжтой"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Бүртгүүлэхийн тулд товшино уу"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Холбогдсон хэдий ч интернет алга"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Хувийн DNS серверт хандах боломжгүй байна"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Хязгаарлагдмал холболт"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Интернэт алга"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Нэвтрэх шаардлагатай"</string>
diff --git a/packages/SettingsLib/res/values-mr/strings.xml b/packages/SettingsLib/res/values-mr/strings.xml
index 3d75ad6d158f..751010759fd0 100644
--- a/packages/SettingsLib/res/values-mr/strings.xml
+++ b/packages/SettingsLib/res/values-mr/strings.xml
@@ -43,6 +43,8 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s द्वारे उपलब्‍ध"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"साइन अप करण्यासाठी टॅप करा"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"कनेक्‍ट केले, इंटरनेट नाही"</string>
+ <!-- no translation found for private_dns_broken (7356676011023412490) -->
+ <skip />
<string name="wifi_limited_connection" msgid="7717855024753201527">"मर्यादित कनेक्शन"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"इंटरनेट नाही"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"साइन इन करणे आवश्यक आहे"</string>
diff --git a/packages/SettingsLib/res/values-ms/strings.xml b/packages/SettingsLib/res/values-ms/strings.xml
index 0b2a4b0879c4..82bd697a6a52 100644
--- a/packages/SettingsLib/res/values-ms/strings.xml
+++ b/packages/SettingsLib/res/values-ms/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Tersedia melalui %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Ketik untuk daftar"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Disambungkan, tiada Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Pelayan DNS peribadi tidak boleh diakses"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Sambungan terhad"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Tiada Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Log masuk diperlukan"</string>
diff --git a/packages/SettingsLib/res/values-my/strings.xml b/packages/SettingsLib/res/values-my/strings.xml
index 6fde69a38767..9636f0675dd4 100644
--- a/packages/SettingsLib/res/values-my/strings.xml
+++ b/packages/SettingsLib/res/values-my/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s မှတစ်ဆင့်ရနိုင်သည်"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"အကောင့်ဖွင့်ရန် တို့ပါ"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"ချိတ်ဆက်ထားသည်၊ အင်တာနက်မရှိ"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"သီးသန့် ဒီအန်အက်စ် (DNS) ဆာဗာကို သုံး၍မရပါ။"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"ချိတ်ဆက်မှု ကန့်သတ်ထားသည်"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"အင်တာနက် မရှိပါ"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"လက်မှတ်ထိုးဝင်ရန် လိုအပ်သည်"</string>
diff --git a/packages/SettingsLib/res/values-nb/strings.xml b/packages/SettingsLib/res/values-nb/strings.xml
index 66ad20e18839..99af7c88598a 100644
--- a/packages/SettingsLib/res/values-nb/strings.xml
+++ b/packages/SettingsLib/res/values-nb/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Tilgjengelig via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Trykk for å registrere deg"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Tilkoblet – ingen Internett-tilgang"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Den private DNS-tjeneren kan ikke nås"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Begrenset tilkobling"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Ingen internettilkobling"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Pålogging kreves"</string>
diff --git a/packages/SettingsLib/res/values-ne/strings.xml b/packages/SettingsLib/res/values-ne/strings.xml
index 860e9bf6b1b7..cdf2c7d27134 100644
--- a/packages/SettingsLib/res/values-ne/strings.xml
+++ b/packages/SettingsLib/res/values-ne/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s मार्फत उपलब्ध"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"साइन अप गर्न ट्याप गर्नुहोस्"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"जडान गरियो तर इन्टरनेट छैन"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"निजी DNS सर्भरमाथि पहुँच प्राप्त गर्न सकिँदैन"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"सीमित जडान"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"इन्टरनेटमाथिको पहुँच छैन"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"साइन इन गर्न आवश्यक छ"</string>
diff --git a/packages/SettingsLib/res/values-nl/strings.xml b/packages/SettingsLib/res/values-nl/strings.xml
index 2fff3283d5fd..709e98d5b2c7 100644
--- a/packages/SettingsLib/res/values-nl/strings.xml
+++ b/packages/SettingsLib/res/values-nl/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Beschikbaar via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Tik om aan te melden"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Verbonden, geen internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Geen toegang tot privé-DNS-server"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Beperkte verbinding"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Geen internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Inloggen vereist"</string>
diff --git a/packages/SettingsLib/res/values-or/strings.xml b/packages/SettingsLib/res/values-or/strings.xml
index ff2a5349f40f..abe01984e9ef 100644
--- a/packages/SettingsLib/res/values-or/strings.xml
+++ b/packages/SettingsLib/res/values-or/strings.xml
@@ -43,6 +43,8 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s ମାଧ୍ୟମରେ ଉପଲବ୍ଧ"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"ସାଇନ୍ ଅପ୍ ପାଇଁ ଟାପ୍ କରନ୍ତୁ"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"ସଂଯୁକ୍ତ, ଇଣ୍ଟର୍‌ନେଟ୍‌ ନାହିଁ"</string>
+ <!-- no translation found for private_dns_broken (7356676011023412490) -->
+ <skip />
<string name="wifi_limited_connection" msgid="7717855024753201527">"ସୀମିତ ସଂଯୋଗ"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"କୌଣସି ଇଣ୍ଟରନେଟ୍‌ ନାହିଁ"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"ସାଇନ୍-ଇନ୍ ଆବଶ୍ୟକ"</string>
diff --git a/packages/SettingsLib/res/values-pa/strings.xml b/packages/SettingsLib/res/values-pa/strings.xml
index 0cf95753ae18..b372185be7f2 100644
--- a/packages/SettingsLib/res/values-pa/strings.xml
+++ b/packages/SettingsLib/res/values-pa/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s ਰਾਹੀਂ ਉਪਲਬਧ"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"ਸਾਈਨ-ਅੱਪ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"ਕਨੈਕਟ ਕੀਤਾ, ਕੋਈ ਇੰਟਰਨੈੱਟ ਨਹੀਂ"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"ਨਿੱਜੀ ਡੋਮੇਨ ਨਾਮ ਪ੍ਰਣਾਲੀ (DNS) ਸਰਵਰ \'ਤੇ ਪਹੁੰਚ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕੀ"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"ਸੀਮਤ ਕਨੈਕਸ਼ਨ"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ਇੰਟਰਨੈੱਟ ਨਹੀਂ"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"ਸਾਈਨ-ਇਨ ਲੋੜੀਂਦਾ ਹੈ"</string>
diff --git a/packages/SettingsLib/res/values-pl/strings.xml b/packages/SettingsLib/res/values-pl/strings.xml
index caed8c349af7..e7a8f22b52a5 100644
--- a/packages/SettingsLib/res/values-pl/strings.xml
+++ b/packages/SettingsLib/res/values-pl/strings.xml
@@ -43,6 +43,8 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Dostępne przez %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Kliknij, by się zarejestrować"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Połączono, brak internetu"</string>
+ <!-- no translation found for private_dns_broken (7356676011023412490) -->
+ <skip />
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ograniczone połączenie"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Brak internetu"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Musisz się zalogować"</string>
diff --git a/packages/SettingsLib/res/values-pt-rBR/strings.xml b/packages/SettingsLib/res/values-pt-rBR/strings.xml
index 04a24f1a9175..6e75fbaa9d3f 100644
--- a/packages/SettingsLib/res/values-pt-rBR/strings.xml
+++ b/packages/SettingsLib/res/values-pt-rBR/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Disponível via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Toque para se inscrever"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Conectada, sem Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Não é possível acessar o servidor DNS privado"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Conexão limitada"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Sem Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"É necessário fazer login"</string>
diff --git a/packages/SettingsLib/res/values-pt-rPT/strings.xml b/packages/SettingsLib/res/values-pt-rPT/strings.xml
index a206d1ad787c..4ee0a17cec74 100644
--- a/packages/SettingsLib/res/values-pt-rPT/strings.xml
+++ b/packages/SettingsLib/res/values-pt-rPT/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Disponível através de %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Toque para se inscrever"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Ligado, sem Internet."</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Não é possível aceder ao servidor DNS."</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ligação limitada"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Sem Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"É necessário iniciar sessão"</string>
diff --git a/packages/SettingsLib/res/values-pt/strings.xml b/packages/SettingsLib/res/values-pt/strings.xml
index 04a24f1a9175..6e75fbaa9d3f 100644
--- a/packages/SettingsLib/res/values-pt/strings.xml
+++ b/packages/SettingsLib/res/values-pt/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Disponível via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Toque para se inscrever"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Conectada, sem Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Não é possível acessar o servidor DNS privado"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Conexão limitada"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Sem Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"É necessário fazer login"</string>
diff --git a/packages/SettingsLib/res/values-ro/strings.xml b/packages/SettingsLib/res/values-ro/strings.xml
index 76a56e51ef88..c5725d3262d7 100644
--- a/packages/SettingsLib/res/values-ro/strings.xml
+++ b/packages/SettingsLib/res/values-ro/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Disponibilă prin %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Atingeți pentru a vă înscrie"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Conectată, fără internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Serverul DNS privat nu poate fi accesat"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Conexiune limitată"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Fără conexiune la internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Trebuie să vă conectați"</string>
diff --git a/packages/SettingsLib/res/values-ru/strings.xml b/packages/SettingsLib/res/values-ru/strings.xml
index 6e1d29ad13d8..6e98601fbcd9 100644
--- a/packages/SettingsLib/res/values-ru/strings.xml
+++ b/packages/SettingsLib/res/values-ru/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Доступно через %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Нажмите, чтобы зарегистрироваться"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Подключено, без доступа к Интернету"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Доступа к частному DNS-серверу нет."</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Подключение к сети ограничено."</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Нет подключения к Интернету"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Требуется выполнить вход."</string>
diff --git a/packages/SettingsLib/res/values-si/strings.xml b/packages/SettingsLib/res/values-si/strings.xml
index 186c23adaec1..0f67a6517ccc 100644
--- a/packages/SettingsLib/res/values-si/strings.xml
+++ b/packages/SettingsLib/res/values-si/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s හරහා ලබා ගැනීමට හැකිය"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"ලියාපදිංචි වීමට තට්ටු කරන්න"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"සම්බන්ධයි, අන්තර්ජාලය නැත"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"පුද්ගලික DNS සේවාදායකයට ප්‍රවේශ වීමට නොහැකිය"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"සීමිත සම්බන්ධතාව"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"අන්තර්ජාලය නැත"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"පිරීම අවශ්‍යයි"</string>
diff --git a/packages/SettingsLib/res/values-sk/strings.xml b/packages/SettingsLib/res/values-sk/strings.xml
index 9559975cb156..e4b48487688d 100644
--- a/packages/SettingsLib/res/values-sk/strings.xml
+++ b/packages/SettingsLib/res/values-sk/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"K dispozícii prostredníctvom %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Prihláste sa klepnutím"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Pripojené, žiadny internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"K súkromnému serveru DNS sa nepodarilo získať prístup"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Obmedzené pripojenie"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Žiadny internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Vyžaduje sa prihlásenie"</string>
diff --git a/packages/SettingsLib/res/values-sl/strings.xml b/packages/SettingsLib/res/values-sl/strings.xml
index 62e4ad1f91f4..3e181c2cacb0 100644
--- a/packages/SettingsLib/res/values-sl/strings.xml
+++ b/packages/SettingsLib/res/values-sl/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Na voljo prek: %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Dotaknite se, če se želite registrirati"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Vzpostavljena povezava, brez interneta"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Do zasebnega strežnika DNS ni mogoče dostopati"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Omejena povezava"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Brez internetne povezave"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Zahtevana je prijava"</string>
diff --git a/packages/SettingsLib/res/values-sq/strings.xml b/packages/SettingsLib/res/values-sq/strings.xml
index 0498f3123543..53803022f388 100644
--- a/packages/SettingsLib/res/values-sq/strings.xml
+++ b/packages/SettingsLib/res/values-sq/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"E mundshme përmes %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Trokit për t\'u regjistruar"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"U lidh, por nuk ka internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Serveri privat DNS nuk mund të qaset"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Lidhje e kufizuar"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Nuk ka internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Kërkohet identifikimi"</string>
diff --git a/packages/SettingsLib/res/values-sr/strings.xml b/packages/SettingsLib/res/values-sr/strings.xml
index 3157deed7127..d68c9e8a1f22 100644
--- a/packages/SettingsLib/res/values-sr/strings.xml
+++ b/packages/SettingsLib/res/values-sr/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Доступна је преко приступне тачке %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Додирните да бисте се регистровали"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Веза је успостављена, нема интернета"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Приступ приватном DNS серверу није успео"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Ограничена веза"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Нема интернета"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Треба да се пријавите"</string>
diff --git a/packages/SettingsLib/res/values-sv/strings.xml b/packages/SettingsLib/res/values-sv/strings.xml
index 31517b8ab00a..45841f0ee816 100644
--- a/packages/SettingsLib/res/values-sv/strings.xml
+++ b/packages/SettingsLib/res/values-sv/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Tillgängligt via %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Tryck för att logga in"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Ansluten, inget internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Det går inte att komma åt den privata DNS-servern."</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Begränsad anslutning"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Inget internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Inloggning krävs"</string>
diff --git a/packages/SettingsLib/res/values-sw/strings.xml b/packages/SettingsLib/res/values-sw/strings.xml
index d73084ffd3e8..a8e73d75dad2 100644
--- a/packages/SettingsLib/res/values-sw/strings.xml
+++ b/packages/SettingsLib/res/values-sw/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Inapatikana kupitia %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Gusa ili ujisajili"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Imeunganishwa, hakuna intaneti"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Seva ya faragha ya DNS haiwezi kufikiwa"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Muunganisho hafifu"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Hakuna intaneti"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Unahitaji kuingia katika akaunti"</string>
diff --git a/packages/SettingsLib/res/values-ta/strings.xml b/packages/SettingsLib/res/values-ta/strings.xml
index de70d085d491..83fe954dcac8 100644
--- a/packages/SettingsLib/res/values-ta/strings.xml
+++ b/packages/SettingsLib/res/values-ta/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s வழியாகக் கிடைக்கிறது"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"பதிவு செய்யத் தட்டவும்"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"இணைக்கப்பட்டுள்ளது, ஆனால் இண்டர்நெட் இல்லை"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"தனிப்பட்ட DNS சேவையகத்தை அணுக இயலாது"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"வரம்பிற்கு உட்பட்ட இணைப்பு"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"இணைய இணைப்பு இல்லை"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"உள்நுழைய வேண்டும்"</string>
diff --git a/packages/SettingsLib/res/values-te/strings.xml b/packages/SettingsLib/res/values-te/strings.xml
index 03ae94f39aef..ad0f673886c1 100644
--- a/packages/SettingsLib/res/values-te/strings.xml
+++ b/packages/SettingsLib/res/values-te/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s ద్వారా అందుబాటులో ఉంది"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"సైన్ అప్ చేయడానికి నొక్కండి"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"కనెక్ట్ చేయబడింది, ఇంటర్నెట్ లేదు"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"ప్రైవేట్ DNS సర్వర్‌ను యాక్సెస్ చేయడం సాధ్యపడదు"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"పరిమిత కనెక్షన్"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ఇంటర్నెట్ లేదు"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"సైన్ ఇన్ చేయాలి"</string>
diff --git a/packages/SettingsLib/res/values-th/strings.xml b/packages/SettingsLib/res/values-th/strings.xml
index 1cfe45e72a9e..c9e232d45a85 100644
--- a/packages/SettingsLib/res/values-th/strings.xml
+++ b/packages/SettingsLib/res/values-th/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"พร้อมใช้งานผ่านทาง %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"แตะเพื่อลงชื่อสมัครใช้"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"เชื่อมต่อแล้ว ไม่พบอินเทอร์เน็ต"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"เข้าถึงเซิร์ฟเวอร์ DNS ไม่ได้"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"การเชื่อมต่อที่จำกัด"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"ไม่มีอินเทอร์เน็ต"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"ต้องลงชื่อเข้าใช้"</string>
diff --git a/packages/SettingsLib/res/values-tl/strings.xml b/packages/SettingsLib/res/values-tl/strings.xml
index 8b3118b333d1..74307f8e3912 100644
--- a/packages/SettingsLib/res/values-tl/strings.xml
+++ b/packages/SettingsLib/res/values-tl/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Available sa pamamagitan ng %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"I-tap para mag-sign up"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Nakakonekta, walang internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Hindi ma-access ang pribadong DNS server"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Limitadong koneksyon"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Walang internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Kinakailangang mag-sign in"</string>
diff --git a/packages/SettingsLib/res/values-tr/strings.xml b/packages/SettingsLib/res/values-tr/strings.xml
index 5891c791cec9..b95d941a83b3 100644
--- a/packages/SettingsLib/res/values-tr/strings.xml
+++ b/packages/SettingsLib/res/values-tr/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s üzerinden kullanılabilir"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Kaydolmak için dokunun"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Bağlı, internet yok"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Gizli DNS sunucusuna erişilemiyor"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Sınırlı bağlantı"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"İnternet yok"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Oturum açılması gerekiyor"</string>
diff --git a/packages/SettingsLib/res/values-uk/strings.xml b/packages/SettingsLib/res/values-uk/strings.xml
index 0bca3077a6ad..61a0c3e6128e 100644
--- a/packages/SettingsLib/res/values-uk/strings.xml
+++ b/packages/SettingsLib/res/values-uk/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Доступ через %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Торкніться, щоб увійти"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Під’єднано, але немає доступу до Інтернету"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Немає доступу до приватного DNS-сервера"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Обмежене з’єднання"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Немає Інтернету"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Потрібно ввійти в обліковий запис"</string>
diff --git a/packages/SettingsLib/res/values-ur/strings.xml b/packages/SettingsLib/res/values-ur/strings.xml
index 1e27b487ca27..f21e891fb9ae 100644
--- a/packages/SettingsLib/res/values-ur/strings.xml
+++ b/packages/SettingsLib/res/values-ur/strings.xml
@@ -43,6 +43,8 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"‏دستیاب بذریعہ ‎%1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"سائن اپ کے لیے تھپتھپائیں"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"منسلک، انٹرنیٹ نہیں ہے"</string>
+ <!-- no translation found for private_dns_broken (7356676011023412490) -->
+ <skip />
<string name="wifi_limited_connection" msgid="7717855024753201527">"محدود کنکشن"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"انٹرنیٹ نہیں ہے"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"سائن ان درکار ہے"</string>
diff --git a/packages/SettingsLib/res/values-uz/strings.xml b/packages/SettingsLib/res/values-uz/strings.xml
index 487100c94feb..a0a79e38422f 100644
--- a/packages/SettingsLib/res/values-uz/strings.xml
+++ b/packages/SettingsLib/res/values-uz/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"%1$s orqali ishlaydi"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Yozilish uchun bosing"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Ulangan, lekin internet aloqasi yo‘q"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Xususiy DNS server ishlamayapti"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Cheklangan aloqa"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Internet yo‘q"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Hisob bilan kirish zarur"</string>
diff --git a/packages/SettingsLib/res/values-vi/strings.xml b/packages/SettingsLib/res/values-vi/strings.xml
index c247617b31dc..3723b83db79d 100644
--- a/packages/SettingsLib/res/values-vi/strings.xml
+++ b/packages/SettingsLib/res/values-vi/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Có sẵn qua %1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Nhấn để đăng ký"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Đã kết nối, không có Internet"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Không thể truy cập máy chủ DNS riêng tư"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Kết nối giới hạn"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Không có Internet"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Yêu cầu đăng nhập"</string>
diff --git a/packages/SettingsLib/res/values-zh-rCN/strings.xml b/packages/SettingsLib/res/values-zh-rCN/strings.xml
index dddc1071dd9b..f1200ee13404 100644
--- a/packages/SettingsLib/res/values-zh-rCN/strings.xml
+++ b/packages/SettingsLib/res/values-zh-rCN/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"可通过%1$s连接"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"点按即可注册"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"已连接,但无法访问互联网"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"无法访问私人 DNS 服务器"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"网络连接受限"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"无法访问互联网"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"必须登录"</string>
diff --git a/packages/SettingsLib/res/values-zh-rHK/strings.xml b/packages/SettingsLib/res/values-zh-rHK/strings.xml
index 8560e22c587b..57ab472b5d3f 100644
--- a/packages/SettingsLib/res/values-zh-rHK/strings.xml
+++ b/packages/SettingsLib/res/values-zh-rHK/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"可透過 %1$s 連線"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"輕按即可登入"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"已連線,但沒有互聯網"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"無法存取私人 DNS 伺服器"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"連線受限"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"沒有互聯網連線"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"必須登入"</string>
diff --git a/packages/SettingsLib/res/values-zh-rTW/strings.xml b/packages/SettingsLib/res/values-zh-rTW/strings.xml
index 404aa19c87c6..630619bba7ab 100644
--- a/packages/SettingsLib/res/values-zh-rTW/strings.xml
+++ b/packages/SettingsLib/res/values-zh-rTW/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"可透過 %1$s 使用"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"輕觸即可註冊"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"已連線,沒有網際網路"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"無法存取私人 DNS 伺服器"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"連線能力受限"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"沒有網際網路連線"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"必須登入"</string>
diff --git a/packages/SettingsLib/res/values-zu/strings.xml b/packages/SettingsLib/res/values-zu/strings.xml
index 542332f10454..ede336e831f9 100644
--- a/packages/SettingsLib/res/values-zu/strings.xml
+++ b/packages/SettingsLib/res/values-zu/strings.xml
@@ -43,6 +43,7 @@
<string name="available_via_passpoint" msgid="1617440946846329613">"Iyatholakala nge-%1$s"</string>
<string name="tap_to_sign_up" msgid="6449724763052579434">"Thepha ukuze ubhalisele"</string>
<string name="wifi_connected_no_internet" msgid="8202906332837777829">"Kuxhunyiwe, ayikho i-inthanethi"</string>
+ <string name="private_dns_broken" msgid="7356676011023412490">"Iseva eyimfihlo ye-DNS ayikwazi ukufinyelelwa"</string>
<string name="wifi_limited_connection" msgid="7717855024753201527">"Iqoqo elikhawulelwe"</string>
<string name="wifi_status_no_internet" msgid="5784710974669608361">"Ayikho i-inthanethi"</string>
<string name="wifi_status_sign_in_required" msgid="123517180404752756">"Ukungena ngemvume kuyadingeka"</string>
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 80faf4766e36..fdc987fb0919 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -1080,6 +1080,9 @@ public class SettingsProvider extends ContentProvider {
Slog.v(LOG_TAG, "getAllConfigFlags() for " + prefix);
}
+ DeviceConfig.enforceReadPermission(getContext(),
+ prefix != null ? prefix.split("/")[0] : null);
+
synchronized (mLock) {
// Get the settings.
SettingsState settingsState = mSettingsRegistry.getSettingsLocked(
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java
index 6674c12ab613..9032c6fc8639 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DependencyBinder.java
@@ -20,6 +20,7 @@ import com.android.systemui.ActivityStarterDelegate;
import com.android.systemui.appops.AppOpsController;
import com.android.systemui.appops.AppOpsControllerImpl;
import com.android.systemui.classifier.FalsingManagerProxy;
+import com.android.systemui.doze.DozeHost;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.FalsingManager;
@@ -33,6 +34,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.phone.DarkIconDispatcherImpl;
+import com.android.systemui.statusbar.phone.DozeServiceHost;
import com.android.systemui.statusbar.phone.ManagedProfileController;
import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl;
import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -241,5 +243,10 @@ public abstract class DependencyBinder {
/**
*/
@Binds
- public abstract FalsingManager provideFalsingmanager(FalsingManagerProxy falsingManagerImpl);
+ public abstract FalsingManager provideFalsingManager(FalsingManagerProxy falsingManagerImpl);
+
+ /**
+ */
+ @Binds
+ public abstract DozeHost provideDozeHost(DozeServiceHost dozeServiceHost);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index 033171a28c62..1e8e28fd1614 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -55,10 +55,10 @@ import javax.inject.Singleton;
@Singleton
public class BiometricUnlockController extends KeyguardUpdateMonitorCallback {
- private static final String TAG = "BiometricUnlockController";
+ private static final String TAG = "BiometricUnlockCtrl";
private static final boolean DEBUG_BIO_WAKELOCK = KeyguardConstants.DEBUG_BIOMETRIC_WAKELOCK;
private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000;
- private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock wakelock";
+ private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock";
@IntDef(prefix = { "MODE_" }, value = {
MODE_NONE,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
index 50d33a70fed5..bc482353753d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -24,7 +24,6 @@ import android.os.UserHandle;
import android.provider.Settings;
import android.util.MathUtils;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.MainResources;
import com.android.systemui.doze.AlwaysOnDisplayPolicy;
@@ -188,12 +187,7 @@ public class DozeParameters implements TunerService.Tunable,
return;
}
mControlScreenOffAnimation = controlScreenOffAnimation;
- getPowerManager().setDozeAfterScreenOff(!controlScreenOffAnimation);
- }
-
- @VisibleForTesting
- protected PowerManager getPowerManager() {
- return mPowerManager;
+ mPowerManager.setDozeAfterScreenOff(!controlScreenOffAnimation);
}
private boolean getBoolean(String propName, int resId) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
index fe3c04e3cda3..1ecc4899d5e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeScrimController.java
@@ -29,10 +29,12 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import javax.inject.Inject;
+import javax.inject.Singleton;
/**
* Controller which handles all the doze animations of the scrims.
*/
+@Singleton
public class DozeScrimController implements StateListener {
private static final String TAG = "DozeScrimController";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
new file mode 100644
index 000000000000..28543555bf4d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -0,0 +1,465 @@
+/*
+ * 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.phone;
+
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
+import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
+
+import android.annotation.NonNull;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.doze.DozeEvent;
+import com.android.systemui.doze.DozeHost;
+import com.android.systemui.doze.DozeLog;
+import com.android.systemui.doze.DozeReceiver;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.statusbar.PulseExpansionHandler;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+
+import java.util.ArrayList;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Lazy;
+
+/**
+ * Implementation of DozeHost for SystemUI.
+ */
+@Singleton
+public final class DozeServiceHost implements DozeHost {
+ private static final String TAG = "DozeServiceHost";
+ private final ArrayList<Callback> mCallbacks = new ArrayList<>();
+ private final DozeLog mDozeLog;
+ private final PowerManager mPowerManager;
+ private boolean mAnimateWakeup;
+ private boolean mAnimateScreenOff;
+ private boolean mIgnoreTouchWhilePulsing;
+ private Runnable mPendingScreenOffCallback;
+ @VisibleForTesting
+ boolean mWakeLockScreenPerformsAuth = SystemProperties.getBoolean(
+ "persist.sysui.wake_performs_auth", true);
+ private boolean mDozingRequested;
+ private boolean mDozing;
+ private boolean mPulsing;
+ private WakefulnessLifecycle mWakefulnessLifecycle;
+ private final SysuiStatusBarStateController mStatusBarStateController;
+ private final DeviceProvisionedController mDeviceProvisionedController;
+ private final HeadsUpManagerPhone mHeadsUpManagerPhone;
+ private final BatteryController mBatteryController;
+ private final ScrimController mScrimController;
+ private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
+ private BiometricUnlockController mBiometricUnlockController;
+ private final KeyguardViewMediator mKeyguardViewMediator;
+ private final AssistManager mAssistManager;
+ private final DozeScrimController mDozeScrimController;
+ private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ private final VisualStabilityManager mVisualStabilityManager;
+ private final PulseExpansionHandler mPulseExpansionHandler;
+ private final StatusBarWindowController mStatusBarWindowController;
+ private final NotificationWakeUpCoordinator mNotificationWakeUpCoordinator;
+ private NotificationIconAreaController mNotificationIconAreaController;
+ private StatusBarWindowViewController mStatusBarWindowViewController;
+ private StatusBarWindowView mStatusBarWindow;
+ private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ private NotificationPanelView mNotificationPanel;
+ private View mAmbientIndicationContainer;
+ private StatusBar mStatusBar;
+
+ @Inject
+ public DozeServiceHost(DozeLog dozeLog, PowerManager powerManager,
+ WakefulnessLifecycle wakefulnessLifecycle,
+ SysuiStatusBarStateController statusBarStateController,
+ DeviceProvisionedController deviceProvisionedController,
+ HeadsUpManagerPhone headsUpManagerPhone, BatteryController batteryController,
+ ScrimController scrimController,
+ Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
+ KeyguardViewMediator keyguardViewMediator,
+ AssistManager assistManager,
+ DozeScrimController dozeScrimController, KeyguardUpdateMonitor keyguardUpdateMonitor,
+ VisualStabilityManager visualStabilityManager,
+ PulseExpansionHandler pulseExpansionHandler,
+ StatusBarWindowController statusBarWindowController,
+ NotificationWakeUpCoordinator notificationWakeUpCoordinator) {
+ super();
+ mDozeLog = dozeLog;
+ mPowerManager = powerManager;
+ mWakefulnessLifecycle = wakefulnessLifecycle;
+ mStatusBarStateController = statusBarStateController;
+ mDeviceProvisionedController = deviceProvisionedController;
+ mHeadsUpManagerPhone = headsUpManagerPhone;
+ mBatteryController = batteryController;
+ mScrimController = scrimController;
+ mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
+ mKeyguardViewMediator = keyguardViewMediator;
+ mAssistManager = assistManager;
+ mDozeScrimController = dozeScrimController;
+ mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+ mVisualStabilityManager = visualStabilityManager;
+ mPulseExpansionHandler = pulseExpansionHandler;
+ mStatusBarWindowController = statusBarWindowController;
+ mNotificationWakeUpCoordinator = notificationWakeUpCoordinator;
+ }
+
+ // TODO: we should try to not pass status bar in here if we can avoid it.
+
+ /**
+ * Initialize instance with objects only available later during execution.
+ */
+ public void initialize(StatusBar statusBar,
+ NotificationIconAreaController notificationIconAreaController,
+ StatusBarWindowViewController statusBarWindowViewController,
+ StatusBarWindowView statusBarWindow,
+ StatusBarKeyguardViewManager statusBarKeyguardViewManager,
+ NotificationPanelView notificationPanel, View ambientIndicationContainer) {
+ mStatusBar = statusBar;
+ mNotificationIconAreaController = notificationIconAreaController;
+ mStatusBarWindowViewController = statusBarWindowViewController;
+ mStatusBarWindow = statusBarWindow;
+ mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
+ mNotificationPanel = notificationPanel;
+ mAmbientIndicationContainer = ambientIndicationContainer;
+ mBiometricUnlockController = mBiometricUnlockControllerLazy.get();
+ }
+
+ @Override
+ public String toString() {
+ return "PSB.DozeServiceHost[mCallbacks=" + mCallbacks.size() + "]";
+ }
+
+ void firePowerSaveChanged(boolean active) {
+ for (Callback callback : mCallbacks) {
+ callback.onPowerSaveChanged(active);
+ }
+ }
+
+ void fireNotificationPulse(NotificationEntry entry) {
+ Runnable pulseSuppressedListener = () -> {
+ entry.setPulseSuppressed(true);
+ mNotificationIconAreaController.updateAodNotificationIcons();
+ };
+ for (Callback callback : mCallbacks) {
+ callback.onNotificationAlerted(pulseSuppressedListener);
+ }
+ }
+
+ boolean getDozingRequested() {
+ return mDozingRequested;
+ }
+
+ boolean isPulsing() {
+ return mPulsing;
+ }
+
+
+ @Override
+ public void addCallback(@NonNull Callback callback) {
+ mCallbacks.add(callback);
+ }
+
+ @Override
+ public void removeCallback(@NonNull Callback callback) {
+ mCallbacks.remove(callback);
+ }
+
+ @Override
+ public void startDozing() {
+ if (!mDozingRequested) {
+ mDozingRequested = true;
+ mDozeLog.traceDozing(mDozing);
+ updateDozing();
+ mStatusBar.updateIsKeyguard();
+ }
+ }
+
+ void updateDozing() {
+ // When in wake-and-unlock while pulsing, keep dozing state until fully unlocked.
+ boolean
+ dozing =
+ mDozingRequested && mStatusBarStateController.getState() == StatusBarState.KEYGUARD
+ || mBiometricUnlockController.getMode()
+ == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
+ // When in wake-and-unlock we may not have received a change to StatusBarState
+ // but we still should not be dozing, manually set to false.
+ if (mBiometricUnlockController.getMode()
+ == BiometricUnlockController.MODE_WAKE_AND_UNLOCK) {
+ dozing = false;
+ }
+
+
+ mStatusBarStateController.setIsDozing(dozing);
+ }
+
+ @Override
+ public void pulseWhileDozing(@NonNull PulseCallback callback, int reason) {
+ if (reason == DozeEvent.PULSE_REASON_SENSOR_LONG_PRESS) {
+ mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
+ "com.android.systemui:LONG_PRESS");
+ mAssistManager.startAssist(new Bundle());
+ return;
+ }
+
+ if (reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
+ mScrimController.setWakeLockScreenSensorActive(true);
+ }
+
+ if (reason == DozeEvent.PULSE_REASON_DOCKING && mStatusBarWindow != null) {
+ mStatusBarWindowViewController.suppressWakeUpGesture(true);
+ }
+
+ boolean passiveAuthInterrupt = reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN
+ && mWakeLockScreenPerformsAuth;
+ // Set the state to pulsing, so ScrimController will know what to do once we ask it to
+ // execute the transition. The pulse callback will then be invoked when the scrims
+ // are black, indicating that StatusBar is ready to present the rest of the UI.
+ mPulsing = true;
+ mDozeScrimController.pulse(new PulseCallback() {
+ @Override
+ public void onPulseStarted() {
+ callback.onPulseStarted();
+ mStatusBar.updateNotificationPanelTouchState();
+ setPulsing(true);
+ }
+
+ @Override
+ public void onPulseFinished() {
+ mPulsing = false;
+ callback.onPulseFinished();
+ mStatusBar.updateNotificationPanelTouchState();
+ mScrimController.setWakeLockScreenSensorActive(false);
+ if (mStatusBarWindow != null) {
+ mStatusBarWindowViewController.suppressWakeUpGesture(false);
+ }
+ setPulsing(false);
+ }
+
+ private void setPulsing(boolean pulsing) {
+ mStatusBarStateController.setPulsing(pulsing);
+ mStatusBarKeyguardViewManager.setPulsing(pulsing);
+ mKeyguardViewMediator.setPulsing(pulsing);
+ mNotificationPanel.setPulsing(pulsing);
+ mVisualStabilityManager.setPulsing(pulsing);
+ mStatusBarWindowViewController.setPulsing(pulsing);
+ mIgnoreTouchWhilePulsing = false;
+ if (mKeyguardUpdateMonitor != null && passiveAuthInterrupt) {
+ mKeyguardUpdateMonitor.onAuthInterruptDetected(pulsing /* active */);
+ }
+ mStatusBar.updateScrimController();
+ mPulseExpansionHandler.setPulsing(pulsing);
+ mNotificationWakeUpCoordinator.setPulsing(pulsing);
+ }
+ }, reason);
+ // DozeScrimController is in pulse state, now let's ask ScrimController to start
+ // pulsing and draw the black frame, if necessary.
+ mStatusBar.updateScrimController();
+ }
+
+ @Override
+ public void stopDozing() {
+ if (mDozingRequested) {
+ mDozingRequested = false;
+ mDozeLog.traceDozing(mDozing);
+ updateDozing();
+ }
+ }
+
+ @Override
+ public void onIgnoreTouchWhilePulsing(boolean ignore) {
+ if (ignore != mIgnoreTouchWhilePulsing) {
+ mDozeLog.tracePulseTouchDisabledByProx(ignore);
+ }
+ mIgnoreTouchWhilePulsing = ignore;
+ if (mDozing && ignore) {
+ mStatusBarWindowViewController.cancelCurrentTouch();
+ }
+ }
+
+ @Override
+ public void dozeTimeTick() {
+ mNotificationPanel.dozeTimeTick();
+ if (mAmbientIndicationContainer instanceof DozeReceiver) {
+ ((DozeReceiver) mAmbientIndicationContainer).dozeTimeTick();
+ }
+ }
+
+ @Override
+ public boolean isPowerSaveActive() {
+ return mBatteryController.isAodPowerSave();
+ }
+
+ @Override
+ public boolean isPulsingBlocked() {
+ return mBiometricUnlockController.getMode()
+ == BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
+ }
+
+ @Override
+ public boolean isProvisioned() {
+ return mDeviceProvisionedController.isDeviceProvisioned()
+ && mDeviceProvisionedController.isCurrentUserSetup();
+ }
+
+ @Override
+ public boolean isBlockingDoze() {
+ if (mBiometricUnlockController.hasPendingAuthentication()) {
+ Log.i(StatusBar.TAG, "Blocking AOD because fingerprint has authenticated");
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void extendPulse(int reason) {
+ if (reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
+ mScrimController.setWakeLockScreenSensorActive(true);
+ }
+ if (mDozeScrimController.isPulsing() && mHeadsUpManagerPhone.hasNotifications()) {
+ mHeadsUpManagerPhone.extendHeadsUp();
+ } else {
+ mDozeScrimController.extendPulse();
+ }
+ }
+
+ @Override
+ public void stopPulsing() {
+ if (mDozeScrimController.isPulsing()) {
+ mDozeScrimController.pulseOutNow();
+ }
+ }
+
+ @Override
+ public void setAnimateWakeup(boolean animateWakeup) {
+ if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_AWAKE
+ || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_WAKING) {
+ // Too late to change the wakeup animation.
+ return;
+ }
+ mAnimateWakeup = animateWakeup;
+ }
+
+ @Override
+ public void setAnimateScreenOff(boolean animateScreenOff) {
+ mAnimateScreenOff = animateScreenOff;
+ }
+
+ @Override
+ public void onSlpiTap(float screenX, float screenY) {
+ if (screenX > 0 && screenY > 0 && mAmbientIndicationContainer != null
+ && mAmbientIndicationContainer.getVisibility() == View.VISIBLE) {
+ int[] locationOnScreen = new int[2];
+ mAmbientIndicationContainer.getLocationOnScreen(locationOnScreen);
+ float viewX = screenX - locationOnScreen[0];
+ float viewY = screenY - locationOnScreen[1];
+ if (0 <= viewX && viewX <= mAmbientIndicationContainer.getWidth()
+ && 0 <= viewY && viewY <= mAmbientIndicationContainer.getHeight()) {
+
+ // Dispatch a tap
+ long now = SystemClock.elapsedRealtime();
+ MotionEvent ev = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_DOWN, screenX, screenY, 0);
+ mAmbientIndicationContainer.dispatchTouchEvent(ev);
+ ev.recycle();
+ ev = MotionEvent.obtain(
+ now, now, MotionEvent.ACTION_UP, screenX, screenY, 0);
+ mAmbientIndicationContainer.dispatchTouchEvent(ev);
+ ev.recycle();
+ }
+ }
+ }
+
+ @Override
+ public void setDozeScreenBrightness(int value) {
+ mStatusBarWindowController.setDozeScreenBrightness(value);
+ }
+
+ @Override
+ public void setAodDimmingScrim(float scrimOpacity) {
+ mScrimController.setAodFrontScrimAlpha(scrimOpacity);
+ }
+
+
+
+ @Override
+ public void prepareForGentleSleep(Runnable onDisplayOffCallback) {
+ if (mPendingScreenOffCallback != null) {
+ Log.w(TAG, "Overlapping onDisplayOffCallback. Ignoring previous one.");
+ }
+ mPendingScreenOffCallback = onDisplayOffCallback;
+ mStatusBar.updateScrimController();
+ }
+
+ @Override
+ public void cancelGentleSleep() {
+ mPendingScreenOffCallback = null;
+ if (mScrimController.getState() == ScrimState.OFF) {
+ mStatusBar.updateScrimController();
+ }
+ }
+
+ /**
+ * When the dozing host is waiting for scrims to fade out to change the display state.
+ */
+ boolean hasPendingScreenOffCallback() {
+ return mPendingScreenOffCallback != null;
+ }
+
+ /**
+ * Executes an nullifies the pending display state callback.
+ *
+ * @see #hasPendingScreenOffCallback()
+ * @see #prepareForGentleSleep(Runnable)
+ */
+ void executePendingScreenOffCallback() {
+ if (mPendingScreenOffCallback == null) {
+ return;
+ }
+ mPendingScreenOffCallback.run();
+ mPendingScreenOffCallback = null;
+ }
+
+ boolean shouldAnimateWakeup() {
+ return mAnimateWakeup;
+ }
+
+ boolean shouldAnimateScreenOff() {
+ return mAnimateScreenOff;
+ }
+
+ public void setDozing(boolean dozing) {
+ mDozing = dozing;
+ }
+
+ boolean getIgnoreTouchWhilePulsing() {
+ return mIgnoreTouchWhilePulsing;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 6064fbedf63d..35039a0d74f9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -60,11 +60,13 @@ import java.lang.annotation.RetentionPolicy;
import java.util.function.Consumer;
import javax.inject.Inject;
+import javax.inject.Singleton;
/**
* Controls both the scrim behind the notifications and in front of the notifications (when a
* security method gets shown).
*/
+@Singleton
public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnColorsChangedListener,
Dumpable {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 007edfdfc33a..afc147a75c56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -50,7 +50,6 @@ import static com.android.systemui.statusbar.phone.BarTransitions.TransitionMode
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -157,10 +156,8 @@ import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.charging.WirelessChargingAnimation;
import com.android.systemui.classifier.FalsingLog;
import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.doze.DozeEvent;
import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
-import com.android.systemui.doze.DozeReceiver;
import com.android.systemui.fragments.ExtensionFragmentListener;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.keyguard.KeyguardSliceProvider;
@@ -347,7 +344,7 @@ public class StatusBar extends SystemUI implements DemoMode,
/**
* The {@link StatusBarState} of the status bar.
*/
- protected int mState;
+ protected int mState; // TODO: remove this. Just use StatusBarStateController
protected boolean mBouncerShowing;
private PhoneStatusBarPolicy mIconPolicy;
@@ -373,7 +370,7 @@ public class StatusBar extends SystemUI implements DemoMode,
protected StatusBarWindowController mStatusBarWindowController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@VisibleForTesting
- DozeServiceHost mDozeServiceHost = new DozeServiceHost();
+ DozeServiceHost mDozeServiceHost;
private boolean mWakeUpComingFromTouch;
private PointF mWakeUpTouchLocation;
@@ -493,7 +490,6 @@ public class StatusBar extends SystemUI implements DemoMode,
private final UiOffloadThread mUiOffloadThread;
protected boolean mDozing;
- private boolean mDozingRequested;
private final NotificationMediaManager mMediaManager;
private final NotificationLockscreenUserManager mLockscreenUserManager;
@@ -612,7 +608,6 @@ public class StatusBar extends SystemUI implements DemoMode,
private ActivityLaunchAnimator mActivityLaunchAnimator;
protected StatusBarNotificationPresenter mPresenter;
private NotificationActivityStarter mNotificationActivityStarter;
- private boolean mPulsing;
private final BubbleController mBubbleController;
private final BubbleController.BubbleExpandListener mBubbleExpandListener;
@@ -692,7 +687,10 @@ public class StatusBar extends SystemUI implements DemoMode,
DozeParameters dozeParameters,
ScrimController scrimController,
Lazy<LockscreenWallpaper> lockscreenWallpaperLazy,
- Lazy<BiometricUnlockController> biometricUnlockControllerLazy) {
+ Lazy<BiometricUnlockController> biometricUnlockControllerLazy,
+ DozeServiceHost dozeServiceHost,
+ PowerManager powerManager,
+ DozeScrimController dozeScrimController) {
super(context);
mFeatureFlags = featureFlags;
mLightBarController = lightBarController;
@@ -749,9 +747,12 @@ public class StatusBar extends SystemUI implements DemoMode,
mStatusBarWindowController = statusBarWindowController;
mStatusBarWindowViewControllerBuilder = statusBarWindowViewControllerBuilder;
mNotifLog = notifLog;
+ mDozeServiceHost = dozeServiceHost;
+ mPowerManager = powerManager;
mDozeParameters = dozeParameters;
mScrimController = scrimController;
mLockscreenWallpaperLazy = lockscreenWallpaperLazy;
+ mDozeScrimController = dozeScrimController;
mBiometricUnlockControllerLazy = biometricUnlockControllerLazy;
mBubbleExpandListener =
@@ -804,7 +805,6 @@ public class StatusBar extends SystemUI implements DemoMode,
mAccessibilityManager = (AccessibilityManager)
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
- mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController);
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
@@ -904,6 +904,9 @@ public class StatusBar extends SystemUI implements DemoMode,
startKeyguard();
mKeyguardUpdateMonitor.registerCallback(mUpdateCallback);
+ mDozeServiceHost.initialize(this, mNotificationIconAreaController,
+ mStatusBarWindowViewController, mStatusBarWindow, mStatusBarKeyguardViewManager,
+ mNotificationPanel, mAmbientIndicationContainer);
putComponent(DozeHost.class, mDozeServiceHost);
mScreenPinningRequest = new ScreenPinningRequest(mContext);
@@ -1068,7 +1071,6 @@ public class StatusBar extends SystemUI implements DemoMode,
mNotificationPanel.initDependencies(this, mGroupManager, mNotificationShelf,
mHeadsUpManager, mNotificationIconAreaController, mScrimController);
- mDozeScrimController = new DozeScrimController(mDozeParameters, mDozeLog);
BackDropView backdrop = mStatusBarWindow.findViewById(R.id.backdrop);
mMediaManager.setup(backdrop, backdrop.findViewById(R.id.backdrop_front),
@@ -1728,7 +1730,7 @@ public class StatusBar extends SystemUI implements DemoMode,
if (isDozing() && isHeadsUp) {
entry.setPulseSuppressed(false);
mDozeServiceHost.fireNotificationPulse(entry);
- if (mPulsing) {
+ if (mDozeServiceHost.isPulsing()) {
mDozeScrimController.cancelPendingPulseTimeout();
}
}
@@ -1760,7 +1762,7 @@ public class StatusBar extends SystemUI implements DemoMode,
}
public boolean isPulsing() {
- return mPulsing;
+ return mDozeServiceHost.isPulsing();
}
public boolean hideStatusBarIconsWhenExpanded() {
@@ -2823,7 +2825,7 @@ public class StatusBar extends SystemUI implements DemoMode,
if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP
&& mKeyguardStateController.canDismissLockScreen()
&& !mStatusBarStateController.leaveOpenOnKeyguardHide()
- && isPulsing()) {
+ && mDozeServiceHost.isPulsing()) {
// Reuse the biometric wake-and-unlock transition if we dismiss keyguard from a pulse.
// TODO: Factor this transition out of BiometricUnlockController.
mBiometricUnlockController.startWakeAndUnlock(
@@ -3154,7 +3156,7 @@ public class StatusBar extends SystemUI implements DemoMode,
return mState == StatusBarState.FULLSCREEN_USER_SWITCHER;
}
- private boolean updateIsKeyguard() {
+ boolean updateIsKeyguard() {
boolean wakeAndUnlocking = mBiometricUnlockController.getMode()
== BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
@@ -3162,8 +3164,8 @@ public class StatusBar extends SystemUI implements DemoMode,
// there's no surface we can show to the user. Note that the device goes fully interactive
// late in the transition, so we also allow the device to start dozing once the screen has
// turned off fully.
- boolean keyguardForDozing = mDozingRequested &&
- (!mDeviceInteractive || isGoingToSleep() && (isScreenFullyOff() || mIsKeyguard));
+ boolean keyguardForDozing = mDozeServiceHost.getDozingRequested()
+ && (!mDeviceInteractive || isGoingToSleep() && (isScreenFullyOff() || mIsKeyguard));
boolean shouldBeKeyguard = (mStatusBarStateController.isKeyguardRequested()
|| keyguardForDozing) && !wakeAndUnlocking;
if (keyguardForDozing) {
@@ -3579,7 +3581,7 @@ public class StatusBar extends SystemUI implements DemoMode,
public void onStateChanged(int newState) {
mState = newState;
updateReportRejectedTouchVisibility();
- updateDozing();
+ mDozeServiceHost.updateDozing();
updateTheme();
mNavigationBarController.touchAutoDim(mDisplayId);
Trace.beginSection("StatusBar#updateKeyguardState");
@@ -3617,9 +3619,11 @@ public class StatusBar extends SystemUI implements DemoMode,
public void onDozingChanged(boolean isDozing) {
Trace.beginSection("StatusBar#updateDozing");
mDozing = isDozing;
+ mDozeServiceHost.setDozing(mDozing);
// Collapse the notification panel if open
- boolean dozingAnimated = mDozingRequested && mDozeParameters.shouldControlScreenOff();
+ boolean dozingAnimated = mDozeServiceHost.getDozingRequested()
+ && mDozeParameters.shouldControlScreenOff();
mNotificationPanel.resetViews(dozingAnimated);
updateQsExpansionEnabled();
@@ -3627,26 +3631,12 @@ public class StatusBar extends SystemUI implements DemoMode,
mEntryManager.updateNotifications("onDozingChanged");
updateDozingState();
+ mDozeServiceHost.updateDozing();
updateScrimController();
updateReportRejectedTouchVisibility();
Trace.endSection();
}
- private void updateDozing() {
- // When in wake-and-unlock while pulsing, keep dozing state until fully unlocked.
- boolean dozing = mDozingRequested && mState == StatusBarState.KEYGUARD
- || mBiometricUnlockController.getMode()
- == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING;
- // When in wake-and-unlock we may not have received a change to mState
- // but we still should not be dozing, manually set to false.
- if (mBiometricUnlockController.getMode() ==
- BiometricUnlockController.MODE_WAKE_AND_UNLOCK) {
- dozing = false;
- }
-
- mStatusBarStateController.setIsDozing(dozing);
- }
-
private void updateKeyguardState() {
mKeyguardStateController.notifyKeyguardState(mStatusBarKeyguardViewManager.isShowing(),
mStatusBarKeyguardViewManager.isOccluded());
@@ -3871,10 +3861,11 @@ public class StatusBar extends SystemUI implements DemoMode,
* collapse the panel after we expanded it, and thus we would end up with a blank
* Keyguard.
*/
- private void updateNotificationPanelTouchState() {
+ void updateNotificationPanelTouchState() {
boolean goingToSleepWithoutAnimation = isGoingToSleep()
&& !mDozeParameters.shouldControlScreenOff();
- boolean disabled = (!mDeviceInteractive && !mPulsing) || goingToSleepWithoutAnimation;
+ boolean disabled = (!mDeviceInteractive && !mDozeServiceHost.isPulsing())
+ || goingToSleepWithoutAnimation;
mNotificationPanel.setTouchAndAnimationDisabled(disabled);
mNotificationIconAreaController.setAnimationsEnabled(!disabled);
}
@@ -4024,7 +4015,7 @@ public class StatusBar extends SystemUI implements DemoMode,
}
public void notifyBiometricAuthModeChanged() {
- updateDozing();
+ mDozeServiceHost.updateDozing();
updateScrimController();
mStatusBarWindowViewController.onBiometricAuthModeChanged(
mBiometricUnlockController.isWakeAndUnlock(),
@@ -4060,7 +4051,7 @@ public class StatusBar extends SystemUI implements DemoMode,
mScrimController.transitionTo(ScrimState.UNLOCKED, mUnlockScrimCallback);
} else if (mBrightnessMirrorVisible) {
mScrimController.transitionTo(ScrimState.BRIGHTNESS_MIRROR);
- } else if (isPulsing()) {
+ } else if (mDozeServiceHost.isPulsing()) {
mScrimController.transitionTo(ScrimState.PULSING,
mDozeScrimController.getScrimCallback());
} else if (mDozeServiceHost.hasPendingScreenOffCallback()) {
@@ -4090,295 +4081,8 @@ public class StatusBar extends SystemUI implements DemoMode,
return mStatusBarKeyguardViewManager.isShowing();
}
- @VisibleForTesting
- final class DozeServiceHost implements DozeHost {
- private final ArrayList<Callback> mCallbacks = new ArrayList<>();
- private boolean mAnimateWakeup;
- private boolean mAnimateScreenOff;
- private boolean mIgnoreTouchWhilePulsing;
- private Runnable mPendingScreenOffCallback;
- @VisibleForTesting
- boolean mWakeLockScreenPerformsAuth = SystemProperties.getBoolean(
- "persist.sysui.wake_performs_auth", true);
-
- @Override
- public String toString() {
- return "PSB.DozeServiceHost[mCallbacks=" + mCallbacks.size() + "]";
- }
-
- public void firePowerSaveChanged(boolean active) {
- for (Callback callback : mCallbacks) {
- callback.onPowerSaveChanged(active);
- }
- }
-
- public void fireNotificationPulse(NotificationEntry entry) {
- Runnable pulseSupressedListener = () -> {
- entry.setPulseSuppressed(true);
- mNotificationIconAreaController.updateAodNotificationIcons();
- };
- for (Callback callback : mCallbacks) {
- callback.onNotificationAlerted(pulseSupressedListener);
- }
- }
-
- @Override
- public void addCallback(@NonNull Callback callback) {
- mCallbacks.add(callback);
- }
-
- @Override
- public void removeCallback(@NonNull Callback callback) {
- mCallbacks.remove(callback);
- }
-
- @Override
- public void startDozing() {
- if (!mDozingRequested) {
- mDozingRequested = true;
- mDozeLog.traceDozing(mDozing);
- updateDozing();
- updateIsKeyguard();
- }
- }
-
- @Override
- public void pulseWhileDozing(@NonNull PulseCallback callback, int reason) {
- if (reason == DozeEvent.PULSE_REASON_SENSOR_LONG_PRESS) {
- mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE,
- "com.android.systemui:LONG_PRESS");
- startAssist(new Bundle());
- return;
- }
-
- if (reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
- mScrimController.setWakeLockScreenSensorActive(true);
- }
-
- if (reason == DozeEvent.PULSE_REASON_DOCKING && mStatusBarWindow != null) {
- mStatusBarWindowViewController.suppressWakeUpGesture(true);
- }
-
- boolean passiveAuthInterrupt = reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN
- && mWakeLockScreenPerformsAuth;
- // Set the state to pulsing, so ScrimController will know what to do once we ask it to
- // execute the transition. The pulse callback will then be invoked when the scrims
- // are black, indicating that StatusBar is ready to present the rest of the UI.
- mPulsing = true;
- mDozeScrimController.pulse(new PulseCallback() {
- @Override
- public void onPulseStarted() {
- callback.onPulseStarted();
- updateNotificationPanelTouchState();
- setPulsing(true);
- }
-
- @Override
- public void onPulseFinished() {
- mPulsing = false;
- callback.onPulseFinished();
- updateNotificationPanelTouchState();
- mScrimController.setWakeLockScreenSensorActive(false);
- if (mStatusBarWindow != null) {
- mStatusBarWindowViewController.suppressWakeUpGesture(false);
- }
- setPulsing(false);
- }
-
- private void setPulsing(boolean pulsing) {
- mStatusBarStateController.setPulsing(pulsing);
- mStatusBarKeyguardViewManager.setPulsing(pulsing);
- mKeyguardViewMediator.setPulsing(pulsing);
- mNotificationPanel.setPulsing(pulsing);
- mVisualStabilityManager.setPulsing(pulsing);
- mStatusBarWindowViewController.setPulsing(pulsing);
- mIgnoreTouchWhilePulsing = false;
- if (mKeyguardUpdateMonitor != null && passiveAuthInterrupt) {
- mKeyguardUpdateMonitor.onAuthInterruptDetected(pulsing /* active */);
- }
- updateScrimController();
- mPulseExpansionHandler.setPulsing(pulsing);
- mWakeUpCoordinator.setPulsing(pulsing);
- }
- }, reason);
- // DozeScrimController is in pulse state, now let's ask ScrimController to start
- // pulsing and draw the black frame, if necessary.
- updateScrimController();
- }
-
- @Override
- public void stopDozing() {
- if (mDozingRequested) {
- mDozingRequested = false;
- mDozeLog.traceDozing(mDozing);
- updateDozing();
- }
- }
-
- @Override
- public void onIgnoreTouchWhilePulsing(boolean ignore) {
- if (ignore != mIgnoreTouchWhilePulsing) {
- mDozeLog.tracePulseTouchDisabledByProx(ignore);
- }
- mIgnoreTouchWhilePulsing = ignore;
- if (isDozing() && ignore) {
- mStatusBarWindowViewController.cancelCurrentTouch();
- }
- }
-
- @Override
- public void dozeTimeTick() {
- mNotificationPanel.dozeTimeTick();
- if (mAmbientIndicationContainer instanceof DozeReceiver) {
- ((DozeReceiver) mAmbientIndicationContainer).dozeTimeTick();
- }
- }
-
- @Override
- public boolean isPowerSaveActive() {
- return mBatteryController.isAodPowerSave();
- }
-
- @Override
- public boolean isPulsingBlocked() {
- return mBiometricUnlockController.getMode()
- == BiometricUnlockController.MODE_WAKE_AND_UNLOCK;
- }
-
- @Override
- public boolean isProvisioned() {
- return mDeviceProvisionedController.isDeviceProvisioned()
- && mDeviceProvisionedController.isCurrentUserSetup();
- }
-
- @Override
- public boolean isBlockingDoze() {
- if (mBiometricUnlockController.hasPendingAuthentication()) {
- Log.i(TAG, "Blocking AOD because fingerprint has authenticated");
- return true;
- }
- return false;
- }
-
- @Override
- public void extendPulse(int reason) {
- if (reason == DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN) {
- mScrimController.setWakeLockScreenSensorActive(true);
- }
- if (mDozeScrimController.isPulsing() && mHeadsUpManager.hasNotifications()) {
- mHeadsUpManager.extendHeadsUp();
- } else {
- mDozeScrimController.extendPulse();
- }
- }
-
- @Override
- public void stopPulsing() {
- if (mDozeScrimController.isPulsing()) {
- mDozeScrimController.pulseOutNow();
- }
- }
-
- @Override
- public void setAnimateWakeup(boolean animateWakeup) {
- if (mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_AWAKE
- || mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_WAKING) {
- // Too late to change the wakeup animation.
- return;
- }
- mAnimateWakeup = animateWakeup;
- }
-
- @Override
- public void setAnimateScreenOff(boolean animateScreenOff) {
- mAnimateScreenOff = animateScreenOff;
- }
-
- @Override
- public void onSlpiTap(float screenX, float screenY) {
- if (screenX > 0 && screenY > 0 && mAmbientIndicationContainer != null
- && mAmbientIndicationContainer.getVisibility() == View.VISIBLE) {
- mAmbientIndicationContainer.getLocationOnScreen(mTmpInt2);
- float viewX = screenX - mTmpInt2[0];
- float viewY = screenY - mTmpInt2[1];
- if (0 <= viewX && viewX <= mAmbientIndicationContainer.getWidth()
- && 0 <= viewY && viewY <= mAmbientIndicationContainer.getHeight()) {
- dispatchTap(mAmbientIndicationContainer, viewX, viewY);
- }
- }
- }
-
- @Override
- public void setDozeScreenBrightness(int value) {
- mStatusBarWindowController.setDozeScreenBrightness(value);
- }
-
- @Override
- public void setAodDimmingScrim(float scrimOpacity) {
- mScrimController.setAodFrontScrimAlpha(scrimOpacity);
- }
-
- @Override
- public void prepareForGentleSleep(Runnable onDisplayOffCallback) {
- if (mPendingScreenOffCallback != null) {
- Log.w(TAG, "Overlapping onDisplayOffCallback. Ignoring previous one.");
- }
- mPendingScreenOffCallback = onDisplayOffCallback;
- updateScrimController();
- }
-
- @Override
- public void cancelGentleSleep() {
- mPendingScreenOffCallback = null;
- if (mScrimController.getState() == ScrimState.OFF) {
- updateScrimController();
- }
- }
-
- /**
- * When the dozing host is waiting for scrims to fade out to change the display state.
- */
- boolean hasPendingScreenOffCallback() {
- return mPendingScreenOffCallback != null;
- }
-
- /**
- * Executes an nullifies the pending display state callback.
- *
- * @see #hasPendingScreenOffCallback()
- * @see #prepareForGentleSleep(Runnable)
- */
- void executePendingScreenOffCallback() {
- if (mPendingScreenOffCallback == null) {
- return;
- }
- mPendingScreenOffCallback.run();
- mPendingScreenOffCallback = null;
- }
-
- private void dispatchTap(View view, float x, float y) {
- long now = SystemClock.elapsedRealtime();
- dispatchTouchEvent(view, x, y, now, MotionEvent.ACTION_DOWN);
- dispatchTouchEvent(view, x, y, now, MotionEvent.ACTION_UP);
- }
-
- private void dispatchTouchEvent(View view, float x, float y, long now, int action) {
- MotionEvent ev = MotionEvent.obtain(now, now, action, x, y, 0 /* meta */);
- view.dispatchTouchEvent(ev);
- ev.recycle();
- }
-
- private boolean shouldAnimateWakeup() {
- return mAnimateWakeup;
- }
-
- public boolean shouldAnimateScreenOff() {
- return mAnimateScreenOff;
- }
- }
-
public boolean shouldIgnoreTouch() {
- return isDozing() && mDozeServiceHost.mIgnoreTouchWhilePulsing;
+ return isDozing() && mDozeServiceHost.getIgnoreTouchWhilePulsing();
}
// Begin Extra BaseStatusBar methods.
@@ -4405,7 +4109,7 @@ public class StatusBar extends SystemUI implements DemoMode,
private boolean mVisibleToUser;
protected DevicePolicyManager mDevicePolicyManager;
- protected PowerManager mPowerManager;
+ private final PowerManager mPowerManager;
protected StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
protected KeyguardManager mKeyguardManager;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index f2d2faed6a42..2c996684f437 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.policy;
+import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
+
import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import static com.android.systemui.DejankUtils.whitelistIpcs;
@@ -196,7 +198,10 @@ public class UserSwitcherController implements Dumpable {
}
ArrayList<UserRecord> records = new ArrayList<>(infos.size());
int currentId = ActivityManager.getCurrentUser();
- boolean canSwitchUsers = mUserManager.canSwitchUsers();
+ // Check user switchability of the foreground user since SystemUI is running in
+ // User 0
+ boolean canSwitchUsers = mUserManager.getUserSwitchability(
+ UserHandle.of(ActivityManager.getCurrentUser())) == SWITCHABILITY_STATUS_OK;
UserInfo currentUserInfo = null;
UserRecord guestRecord = null;
diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java
index fa3ff64e5e18..0b273274f86d 100644
--- a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java
@@ -35,6 +35,7 @@ import android.util.Log;
import android.widget.CheckBox;
import com.android.internal.app.ResolverActivity;
+import com.android.internal.app.chooser.TargetInfo;
import com.android.systemui.R;
import java.util.ArrayList;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
new file mode 100644
index 000000000000..b05172c6d7c2
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -0,0 +1,224 @@
+/*
+ * 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.phone;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.PowerManager;
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.assist.AssistManager;
+import com.android.systemui.doze.DozeEvent;
+import com.android.systemui.doze.DozeHost;
+import com.android.systemui.doze.DozeLog;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.statusbar.PulseExpansionHandler;
+import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.StatusBarStateControllerImpl;
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
+import com.android.systemui.statusbar.notification.VisualStabilityManager;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+
+import dagger.Lazy;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DozeServiceHostTest extends SysuiTestCase {
+
+ private DozeServiceHost mDozeServiceHost;
+
+ @Mock private HeadsUpManagerPhone mHeadsUpManager;
+ @Mock private ScrimController mScrimController;
+ @Mock private DozeScrimController mDozeScrimController;
+ @Mock private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
+ @Mock private VisualStabilityManager mVisualStabilityManager;
+ @Mock private KeyguardViewMediator mKeyguardViewMediator;
+ @Mock private StatusBarStateControllerImpl mStatusBarStateController;
+ @Mock private BatteryController mBatteryController;
+ @Mock private DeviceProvisionedController mDeviceProvisionedController;
+ @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+ @Mock private AssistManager mAssistManager;
+ @Mock private DozeLog mDozeLog;
+ @Mock private PulseExpansionHandler mPulseExpansionHandler;
+ @Mock private NotificationWakeUpCoordinator mNotificationWakeUpCoordinator;
+ @Mock private StatusBarWindowController mStatusBarWindowController;
+ @Mock private PowerManager mPowerManager;
+ @Mock private WakefulnessLifecycle mWakefullnessLifecycle;
+ @Mock private StatusBar mStatusBar;
+ @Mock private NotificationIconAreaController mNotificationIconAreaController;
+ @Mock private StatusBarWindowViewController mStatusBarWindowViewController;
+ @Mock private StatusBarWindowView mStatusBarWindow;
+ @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ @Mock private NotificationPanelView mNotificationPanel;
+ @Mock private View mAmbientIndicationContainer;
+ @Mock private BiometricUnlockController mBiometricUnlockController;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController);
+ mDozeServiceHost = new DozeServiceHost(mDozeLog, mPowerManager, mWakefullnessLifecycle,
+ mStatusBarStateController, mDeviceProvisionedController, mHeadsUpManager,
+ mBatteryController, mScrimController, mBiometricUnlockControllerLazy,
+ mKeyguardViewMediator, mAssistManager, mDozeScrimController, mKeyguardUpdateMonitor,
+ mVisualStabilityManager, mPulseExpansionHandler, mStatusBarWindowController,
+ mNotificationWakeUpCoordinator);
+
+ mDozeServiceHost.initialize(mStatusBar, mNotificationIconAreaController,
+ mStatusBarWindowViewController, mStatusBarWindow, mStatusBarKeyguardViewManager,
+ mNotificationPanel, mAmbientIndicationContainer);
+ }
+
+ @Test
+ public void testStartStopDozing() {
+ when(mStatusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD);
+ when(mStatusBarStateController.isKeyguardRequested()).thenReturn(true);
+
+ assertFalse(mDozeServiceHost.getDozingRequested());
+
+ mDozeServiceHost.startDozing();
+ verify(mStatusBarStateController).setIsDozing(eq(true));
+ verify(mStatusBar).updateIsKeyguard();
+
+ mDozeServiceHost.stopDozing();
+ verify(mStatusBarStateController).setIsDozing(eq(false));
+ }
+
+
+ @Test
+ public void testPulseWhileDozing_updatesScrimController() {
+ mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
+ mStatusBar.showKeyguardImpl();
+
+ // Keep track of callback to be able to stop the pulse
+// DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
+// doAnswer(invocation -> {
+// pulseCallback[0] = invocation.getArgument(0);
+// return null;
+// }).when(mDozeScrimController).pulse(any(), anyInt());
+
+ // Starting a pulse should change the scrim controller to the pulsing state
+ mDozeServiceHost.pulseWhileDozing(new DozeHost.PulseCallback() {
+ @Override
+ public void onPulseStarted() {
+ }
+
+ @Override
+ public void onPulseFinished() {
+ }
+ }, DozeEvent.PULSE_REASON_NOTIFICATION);
+
+ ArgumentCaptor<DozeHost.PulseCallback> pulseCallbackArgumentCaptor =
+ ArgumentCaptor.forClass(DozeHost.PulseCallback.class);
+
+ verify(mDozeScrimController).pulse(
+ pulseCallbackArgumentCaptor.capture(), eq(DozeEvent.PULSE_REASON_NOTIFICATION));
+ verify(mStatusBar).updateScrimController();
+ reset(mStatusBar);
+
+ pulseCallbackArgumentCaptor.getValue().onPulseFinished();
+ assertFalse(mDozeScrimController.isPulsing());
+ verify(mStatusBar).updateScrimController();
+ }
+
+
+ @Test
+ public void testPulseWhileDozingWithDockingReason_suppressWakeUpGesture() {
+ // Keep track of callback to be able to stop the pulse
+ final DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
+ doAnswer(invocation -> {
+ pulseCallback[0] = invocation.getArgument(0);
+ return null;
+ }).when(mDozeScrimController).pulse(any(), anyInt());
+
+ // Starting a pulse while docking should suppress wakeup gesture
+ mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class),
+ DozeEvent.PULSE_REASON_DOCKING);
+ verify(mStatusBarWindowViewController).suppressWakeUpGesture(eq(true));
+
+ // Ending a pulse should restore wakeup gesture
+ pulseCallback[0].onPulseFinished();
+ verify(mStatusBarWindowViewController).suppressWakeUpGesture(eq(false));
+ }
+
+ @Test
+ public void testPulseWhileDozing_notifyAuthInterrupt() {
+ HashSet<Integer> reasonsWantingAuth = new HashSet<>(
+ Collections.singletonList(DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN));
+ HashSet<Integer> reasonsSkippingAuth = new HashSet<>(
+ Arrays.asList(DozeEvent.PULSE_REASON_INTENT,
+ DozeEvent.PULSE_REASON_NOTIFICATION,
+ DozeEvent.PULSE_REASON_SENSOR_SIGMOTION,
+ DozeEvent.REASON_SENSOR_PICKUP,
+ DozeEvent.REASON_SENSOR_DOUBLE_TAP,
+ DozeEvent.PULSE_REASON_SENSOR_LONG_PRESS,
+ DozeEvent.PULSE_REASON_DOCKING,
+ DozeEvent.REASON_SENSOR_WAKE_UP,
+ DozeEvent.REASON_SENSOR_TAP));
+ HashSet<Integer> reasonsThatDontPulse = new HashSet<>(
+ Arrays.asList(DozeEvent.REASON_SENSOR_PICKUP,
+ DozeEvent.REASON_SENSOR_DOUBLE_TAP,
+ DozeEvent.REASON_SENSOR_TAP));
+
+ doAnswer(invocation -> {
+ DozeHost.PulseCallback callback = invocation.getArgument(0);
+ callback.onPulseStarted();
+ return null;
+ }).when(mDozeScrimController).pulse(any(), anyInt());
+
+ mDozeServiceHost.mWakeLockScreenPerformsAuth = true;
+ for (int i = 0; i < DozeEvent.TOTAL_REASONS; i++) {
+ reset(mKeyguardUpdateMonitor);
+ mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class), i);
+ if (reasonsWantingAuth.contains(i)) {
+ verify(mKeyguardUpdateMonitor).onAuthInterruptDetected(eq(true));
+ } else if (reasonsSkippingAuth.contains(i) || reasonsThatDontPulse.contains(i)) {
+ verify(mKeyguardUpdateMonitor, never()).onAuthInterruptDetected(eq(true));
+ } else {
+ throw new AssertionError("Reason " + i + " isn't specified as wanting or skipping"
+ + " passive auth. Please consider how this pulse reason should behave.");
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index f5e92e4f3181..66c01ca58491 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -85,8 +85,6 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.bubbles.BubbleController;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.doze.DozeEvent;
-import com.android.systemui.doze.DozeHost;
import com.android.systemui.doze.DozeLog;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
@@ -145,9 +143,6 @@ import org.mockito.MockitoAnnotations;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
import dagger.Lazy;
@@ -231,6 +226,7 @@ public class StatusBarTest extends SysuiTestCase {
@Mock private DozeParameters mDozeParameters;
@Mock private Lazy<LockscreenWallpaper> mLockscreenWallpaperLazy;
@Mock private LockscreenWallpaper mLockscreenWallpaper;
+ @Mock private DozeServiceHost mDozeServiceHost;
@Mock private LinearLayout mLockIconContainer;
@Mock private ViewMediatorCallback mKeyguardVieMediatorCallback;
@@ -365,7 +361,10 @@ public class StatusBarTest extends SysuiTestCase {
mDozeParameters,
mScrimController,
mLockscreenWallpaperLazy,
- mBiometricUnlockControllerLazy);
+ mBiometricUnlockControllerLazy,
+ mDozeServiceHost,
+ mPowerManager,
+ mDozeScrimController);
when(mStatusBarWindowView.findViewById(R.id.lock_icon_container)).thenReturn(
mLockIconContainer);
@@ -388,7 +387,6 @@ public class StatusBarTest extends SysuiTestCase {
mStatusBar.mNotificationIconAreaController = mNotificationIconAreaController;
mStatusBar.mPresenter = mNotificationPresenter;
mStatusBar.mKeyguardIndicationController = mKeyguardIndicationController;
- mStatusBar.mPowerManager = mPowerManager;
mStatusBar.mBarService = mBarService;
mStatusBar.mStackScroller = mStackScroller;
mStatusBar.mStatusBarWindowViewController = mStatusBarWindowViewController;
@@ -757,83 +755,18 @@ public class StatusBarTest extends SysuiTestCase {
mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
mStatusBar.showKeyguardImpl();
- // Keep track of callback to be able to stop the pulse
- DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
- doAnswer(invocation -> {
- pulseCallback[0] = invocation.getArgument(0);
- return null;
- }).when(mDozeScrimController).pulse(any(), anyInt());
-
// Starting a pulse should change the scrim controller to the pulsing state
- mStatusBar.mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class),
- DozeEvent.PULSE_REASON_NOTIFICATION);
+ when(mDozeServiceHost.isPulsing()).thenReturn(true);
+ mStatusBar.updateScrimController();
verify(mScrimController).transitionTo(eq(ScrimState.PULSING), any());
// Ending a pulse should take it back to keyguard state
- pulseCallback[0].onPulseFinished();
+ when(mDozeServiceHost.isPulsing()).thenReturn(false);
+ mStatusBar.updateScrimController();
verify(mScrimController).transitionTo(eq(ScrimState.KEYGUARD));
}
@Test
- public void testPulseWhileDozing_notifyAuthInterrupt() {
- HashSet<Integer> reasonsWantingAuth = new HashSet<>(
- Collections.singletonList(DozeEvent.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN));
- HashSet<Integer> reasonsSkippingAuth = new HashSet<>(
- Arrays.asList(DozeEvent.PULSE_REASON_INTENT,
- DozeEvent.PULSE_REASON_NOTIFICATION,
- DozeEvent.PULSE_REASON_SENSOR_SIGMOTION,
- DozeEvent.REASON_SENSOR_PICKUP,
- DozeEvent.REASON_SENSOR_DOUBLE_TAP,
- DozeEvent.PULSE_REASON_SENSOR_LONG_PRESS,
- DozeEvent.PULSE_REASON_DOCKING,
- DozeEvent.REASON_SENSOR_WAKE_UP,
- DozeEvent.REASON_SENSOR_TAP));
- HashSet<Integer> reasonsThatDontPulse = new HashSet<>(
- Arrays.asList(DozeEvent.REASON_SENSOR_PICKUP,
- DozeEvent.REASON_SENSOR_DOUBLE_TAP,
- DozeEvent.REASON_SENSOR_TAP));
-
- doAnswer(invocation -> {
- DozeHost.PulseCallback callback = invocation.getArgument(0);
- callback.onPulseStarted();
- return null;
- }).when(mDozeScrimController).pulse(any(), anyInt());
-
- mStatusBar.mDozeServiceHost.mWakeLockScreenPerformsAuth = true;
- for (int i = 0; i < DozeEvent.TOTAL_REASONS; i++) {
- reset(mKeyguardUpdateMonitor);
- mStatusBar.mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class), i);
- if (reasonsWantingAuth.contains(i)) {
- verify(mKeyguardUpdateMonitor).onAuthInterruptDetected(eq(true));
- } else if (reasonsSkippingAuth.contains(i) || reasonsThatDontPulse.contains(i)) {
- verify(mKeyguardUpdateMonitor, never()).onAuthInterruptDetected(eq(true));
- } else {
- throw new AssertionError("Reason " + i + " isn't specified as wanting or skipping"
- + " passive auth. Please consider how this pulse reason should behave.");
- }
- }
- }
-
- @Test
- public void testPulseWhileDozingWithDockingReason_suppressWakeUpGesture() {
- // Keep track of callback to be able to stop the pulse
- final DozeHost.PulseCallback[] pulseCallback = new DozeHost.PulseCallback[1];
- doAnswer(invocation -> {
- pulseCallback[0] = invocation.getArgument(0);
- return null;
- }).when(mDozeScrimController).pulse(any(), anyInt());
-
- // Starting a pulse while docking should suppress wakeup gesture
- mStatusBar.mDozeServiceHost.pulseWhileDozing(mock(DozeHost.PulseCallback.class),
- DozeEvent.PULSE_REASON_DOCKING);
- verify(mStatusBarWindowViewController).suppressWakeUpGesture(eq(true));
-
- // Ending a pulse should restore wakeup gesture
- pulseCallback[0].onPulseFinished();
- verify(mStatusBarWindowViewController).suppressWakeUpGesture(eq(false));
- }
-
- @Test
public void testSetState_changesIsFullScreenUserSwitcherState() {
mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
assertFalse(mStatusBar.isFullScreenUserSwitcherState());
@@ -859,27 +792,17 @@ public class StatusBarTest extends SysuiTestCase {
}
@Test
- public void testStartStopDozing() {
- mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
- when(mStatusBarStateController.isKeyguardRequested()).thenReturn(true);
-
- mStatusBar.mDozeServiceHost.startDozing();
- verify(mStatusBarStateController).setIsDozing(eq(true));
-
- mStatusBar.mDozeServiceHost.stopDozing();
- verify(mStatusBarStateController).setIsDozing(eq(false));
- }
-
- @Test
public void testOnStartedWakingUp_isNotDozing() {
mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
when(mStatusBarStateController.isKeyguardRequested()).thenReturn(true);
- mStatusBar.mDozeServiceHost.startDozing();
- verify(mStatusBarStateController).setIsDozing(eq(true));
+ when(mDozeServiceHost.getDozingRequested()).thenReturn(true);
+ mStatusBar.updateIsKeyguard();
+ // TODO: mNotificationPanelView.expand(false) gets called twice. Should be once.
+ verify(mNotificationPanelView, times(2)).expand(eq(false));
clearInvocations(mNotificationPanelView);
mStatusBar.mWakefulnessObserver.onStartedWakingUp();
- verify(mStatusBarStateController).setIsDozing(eq(false));
+ verify(mDozeServiceHost).stopDozing();
verify(mNotificationPanelView).expand(eq(false));
}
@@ -887,7 +810,8 @@ public class StatusBarTest extends SysuiTestCase {
public void testOnStartedWakingUp_doesNotDismissBouncer_whenPulsing() {
mStatusBar.setBarStateForTest(StatusBarState.KEYGUARD);
when(mStatusBarStateController.isKeyguardRequested()).thenReturn(true);
- mStatusBar.mDozeServiceHost.startDozing();
+ when(mDozeServiceHost.getDozingRequested()).thenReturn(true);
+ mStatusBar.updateIsKeyguard();
clearInvocations(mNotificationPanelView);
mStatusBar.setBouncerShowing(true);
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index f7ac04041ed6..6010b1dc88c4 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -57,6 +57,13 @@ import java.io.PrintWriter;
private final @NonNull AudioService mAudioService;
private final @NonNull Context mContext;
+ /** Forced device usage for communications sent to AudioSystem */
+ private int mForcedUseForComm;
+ /**
+ * Externally reported force device usage state returned by getters: always consistent
+ * with requests by setters */
+ private int mForcedUseForCommExt;
+
// Manages all connected devices, only ever accessed on the message loop
private final AudioDeviceInventory mDeviceInventory;
// Manages notifications to BT service
@@ -64,34 +71,24 @@ import java.io.PrintWriter;
//-------------------------------------------------------------------
- /**
- * Lock to guard:
- * - any changes to the message queue: enqueueing or removing any message
- * - state of A2DP enabled
- * - force use for communication + SCO changes
- */
- private final Object mDeviceBrokerLock = new Object();
-
- @GuardedBy("mDeviceBrokerLock")
+ // we use a different lock than mDeviceStateLock so as not to create
+ // lock contention between enqueueing a message and handling them
+ private static final Object sLastDeviceConnectionMsgTimeLock = new Object();
+ @GuardedBy("sLastDeviceConnectionMsgTimeLock")
private static long sLastDeviceConnectMsgTime = 0;
+ // General lock to be taken whenever the state of the audio devices is to be checked or changed
+ private final Object mDeviceStateLock = new Object();
- /** Request to override default use of A2DP for media */
- @GuardedBy("mDeviceBrokerLock")
+ // Request to override default use of A2DP for media.
+ @GuardedBy("mDeviceStateLock")
private boolean mBluetoothA2dpEnabled;
- /** Forced device usage for communications sent to AudioSystem */
- @GuardedBy("mDeviceBrokerLock")
- private int mForcedUseForComm;
- /**
- * Externally reported force device usage state returned by getters: always consistent
- * with requests by setters */
- @GuardedBy("mDeviceBrokerLock")
- private int mForcedUseForCommExt;
-
+ // lock always taken when accessing AudioService.mSetModeDeathHandlers
+ // TODO do not "share" the lock between AudioService and BtHelpr, see b/123769055
+ /*package*/ final Object mSetModeLock = new Object();
//-------------------------------------------------------------------
- /** Normal constructor used by AudioService */
/*package*/ AudioDeviceBroker(@NonNull Context context, @NonNull AudioService service) {
mContext = context;
mAudioService = service;
@@ -130,37 +127,38 @@ import java.io.PrintWriter;
// All post* methods are asynchronous
/*package*/ void onSystemReady() {
- mBtHelper.onSystemReady();
+ synchronized (mSetModeLock) {
+ synchronized (mDeviceStateLock) {
+ mBtHelper.onSystemReady();
+ }
+ }
}
/*package*/ void onAudioServerDied() {
// Restore forced usage for communications and record
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
AudioSystem.setParameters(
"BT_SCO=" + (mForcedUseForComm == AudioSystem.FORCE_BT_SCO ? "on" : "off"));
onSetForceUse(AudioSystem.FOR_COMMUNICATION, mForcedUseForComm, "onAudioServerDied");
onSetForceUse(AudioSystem.FOR_RECORD, mForcedUseForComm, "onAudioServerDied");
-
- // restore devices
- sendMsgNoDelay(MSG_RESTORE_DEVICES, SENDMSG_REPLACE);
}
+ // restore devices
+ sendMsgNoDelay(MSG_RESTORE_DEVICES, SENDMSG_REPLACE);
}
/*package*/ void setForceUse_Async(int useCase, int config, String eventSource) {
- synchronized (mDeviceBrokerLock) {
- sendIILMsgNoDelay(MSG_IIL_SET_FORCE_USE, SENDMSG_QUEUE,
- useCase, config, eventSource);
- }
+ sendIILMsgNoDelay(MSG_IIL_SET_FORCE_USE, SENDMSG_QUEUE,
+ useCase, config, eventSource);
}
/*package*/ void toggleHdmiIfConnected_Async() {
- synchronized (mDeviceBrokerLock) {
- sendMsgNoDelay(MSG_TOGGLE_HDMI, SENDMSG_QUEUE);
- }
+ sendMsgNoDelay(MSG_TOGGLE_HDMI, SENDMSG_QUEUE);
}
/*package*/ void disconnectAllBluetoothProfiles() {
+ synchronized (mDeviceStateLock) {
mBtHelper.disconnectAllBluetoothProfiles();
+ }
}
/**
@@ -170,11 +168,15 @@ import java.io.PrintWriter;
* @param intent
*/
/*package*/ void receiveBtEvent(@NonNull Intent intent) {
- mBtHelper.receiveBtEvent(intent);
+ synchronized (mSetModeLock) {
+ synchronized (mDeviceStateLock) {
+ mBtHelper.receiveBtEvent(intent);
+ }
+ }
}
/*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) {
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
if (mBluetoothA2dpEnabled == on) {
return;
}
@@ -194,7 +196,7 @@ import java.io.PrintWriter;
* @return true if speakerphone state changed
*/
/*package*/ boolean setSpeakerphoneOn(boolean on, String eventSource) {
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
final boolean wasOn = isSpeakerphoneOn();
if (on) {
if (mForcedUseForComm == AudioSystem.FORCE_BT_SCO) {
@@ -212,7 +214,7 @@ import java.io.PrintWriter;
}
/*package*/ boolean isSpeakerphoneOn() {
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
return (mForcedUseForCommExt == AudioSystem.FORCE_SPEAKER);
}
}
@@ -221,7 +223,9 @@ import java.io.PrintWriter;
@AudioService.ConnectionState int state, String address, String name,
String caller) {
//TODO move logging here just like in setBluetooth* methods
- mDeviceInventory.setWiredDeviceConnectionState(type, state, address, name, caller);
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.setWiredDeviceConnectionState(type, state, address, name, caller);
+ }
}
private static final class BtDeviceConnectionInfo {
@@ -255,24 +259,27 @@ import java.io.PrintWriter;
final BtDeviceConnectionInfo info = new BtDeviceConnectionInfo(device, state, profile,
suppressNoisyIntent, a2dpVolume);
- synchronized (mDeviceBrokerLock) {
- // when receiving a request to change the connection state of a device, this last
- // request is the source of truth, so cancel all previous requests
- mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
- device);
- mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION,
- device);
- mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
- device);
- mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
- device);
-
- sendLMsgNoDelay(
- state == BluetoothProfile.STATE_CONNECTED
- ? MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION
- : MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
- SENDMSG_QUEUE, info);
- }
+ // when receiving a request to change the connection state of a device, this last request
+ // is the source of truth, so cancel all previous requests
+ removeAllA2dpConnectionEvents(device);
+
+ sendLMsgNoDelay(
+ state == BluetoothProfile.STATE_CONNECTED
+ ? MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION
+ : MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
+ SENDMSG_QUEUE, info);
+ }
+
+ /** remove all previously scheduled connection and disconnection events for the given device */
+ private void removeAllA2dpConnectionEvents(@NonNull BluetoothDevice device) {
+ mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION,
+ device);
+ mBrokerHandler.removeMessages(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION,
+ device);
+ mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
+ device);
+ mBrokerHandler.removeMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+ device);
}
private static final class HearingAidDeviceConnectionInfo {
@@ -298,31 +305,28 @@ import java.io.PrintWriter;
boolean suppressNoisyIntent, int musicDevice, @NonNull String eventSource) {
final HearingAidDeviceConnectionInfo info = new HearingAidDeviceConnectionInfo(
device, state, suppressNoisyIntent, musicDevice, eventSource);
- synchronized (mDeviceBrokerLock) {
- sendLMsgNoDelay(MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
- }
+ sendLMsgNoDelay(MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT, SENDMSG_QUEUE, info);
}
// never called by system components
/*package*/ void setBluetoothScoOnByApp(boolean on) {
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
mForcedUseForCommExt = on ? AudioSystem.FORCE_BT_SCO : AudioSystem.FORCE_NONE;
}
}
/*package*/ boolean isBluetoothScoOnForApp() {
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
return mForcedUseForCommExt == AudioSystem.FORCE_BT_SCO;
}
}
/*package*/ void setBluetoothScoOn(boolean on, String eventSource) {
//Log.i(TAG, "setBluetoothScoOnInt: " + on + " " + eventSource);
- final boolean isBtScoOn = mBtHelper.isBluetoothScoOn();
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
if (on) {
// do not accept SCO ON if SCO audio is not connected
- if (!isBtScoOn) {
+ if (!mBtHelper.isBluetoothScoOn()) {
mForcedUseForCommExt = AudioSystem.FORCE_BT_SCO;
return;
}
@@ -342,55 +346,58 @@ import java.io.PrintWriter;
}
/*package*/ AudioRoutesInfo startWatchingRoutes(IAudioRoutesObserver observer) {
- return mDeviceInventory.startWatchingRoutes(observer);
-
+ synchronized (mDeviceStateLock) {
+ return mDeviceInventory.startWatchingRoutes(observer);
+ }
}
/*package*/ AudioRoutesInfo getCurAudioRoutes() {
- return mDeviceInventory.getCurAudioRoutes();
+ synchronized (mDeviceStateLock) {
+ return mDeviceInventory.getCurAudioRoutes();
+ }
}
/*package*/ boolean isAvrcpAbsoluteVolumeSupported() {
- return mBtHelper.isAvrcpAbsoluteVolumeSupported();
+ synchronized (mDeviceStateLock) {
+ return mBtHelper.isAvrcpAbsoluteVolumeSupported();
+ }
}
/*package*/ boolean isBluetoothA2dpOn() {
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
return mBluetoothA2dpEnabled;
}
}
/*package*/ void postSetAvrcpAbsoluteVolumeIndex(int index) {
- synchronized (mDeviceBrokerLock) {
- sendIMsgNoDelay(MSG_I_SET_AVRCP_ABSOLUTE_VOLUME, SENDMSG_REPLACE, index);
- }
+ sendIMsgNoDelay(MSG_I_SET_AVRCP_ABSOLUTE_VOLUME, SENDMSG_REPLACE, index);
}
/*package*/ void postSetHearingAidVolumeIndex(int index, int streamType) {
- synchronized (mDeviceBrokerLock) {
- sendIIMsgNoDelay(MSG_II_SET_HEARING_AID_VOLUME, SENDMSG_REPLACE, index, streamType);
- }
+ sendIIMsgNoDelay(MSG_II_SET_HEARING_AID_VOLUME, SENDMSG_REPLACE, index, streamType);
}
/*package*/ void postDisconnectBluetoothSco(int exceptPid) {
- synchronized (mDeviceBrokerLock) {
- sendIMsgNoDelay(MSG_I_DISCONNECT_BT_SCO, SENDMSG_REPLACE, exceptPid);
- }
+ sendIMsgNoDelay(MSG_I_DISCONNECT_BT_SCO, SENDMSG_REPLACE, exceptPid);
}
/*package*/ void postBluetoothA2dpDeviceConfigChange(@NonNull BluetoothDevice device) {
- synchronized (mDeviceBrokerLock) {
- sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, device);
- }
+ sendLMsgNoDelay(MSG_L_A2DP_DEVICE_CONFIG_CHANGE, SENDMSG_QUEUE, device);
}
+ @GuardedBy("mSetModeLock")
/*package*/ void startBluetoothScoForClient_Sync(IBinder cb, int scoAudioMode,
@NonNull String eventSource) {
- mBtHelper.startBluetoothScoForClient(cb, scoAudioMode, eventSource);
+ synchronized (mDeviceStateLock) {
+ mBtHelper.startBluetoothScoForClient(cb, scoAudioMode, eventSource);
+ }
}
+ @GuardedBy("mSetModeLock")
/*package*/ void stopBluetoothScoForClient_Sync(IBinder cb, @NonNull String eventSource) {
- mBtHelper.stopBluetoothScoForClient(cb, eventSource);
+ synchronized (mDeviceStateLock) {
+ mBtHelper.stopBluetoothScoForClient(cb, eventSource);
+ }
}
//---------------------------------------------------------------------
@@ -453,109 +460,77 @@ import java.io.PrintWriter;
//---------------------------------------------------------------------
// Message handling on behalf of helper classes
/*package*/ void postBroadcastScoConnectionState(int state) {
- synchronized (mDeviceBrokerLock) {
- sendIMsgNoDelay(MSG_I_BROADCAST_BT_CONNECTION_STATE, SENDMSG_QUEUE, state);
- }
+ sendIMsgNoDelay(MSG_I_BROADCAST_BT_CONNECTION_STATE, SENDMSG_QUEUE, state);
}
/*package*/ void postBroadcastBecomingNoisy() {
- synchronized (mDeviceBrokerLock) {
- sendMsgNoDelay(MSG_BROADCAST_AUDIO_BECOMING_NOISY, SENDMSG_REPLACE);
- }
+ sendMsgNoDelay(MSG_BROADCAST_AUDIO_BECOMING_NOISY, SENDMSG_REPLACE);
}
/*package*/ void postA2dpSinkConnection(@AudioService.BtProfileConnectionState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
- synchronized (mDeviceBrokerLock) {
- sendILMsg(state == BluetoothA2dp.STATE_CONNECTED
- ? MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED
- : MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
- SENDMSG_QUEUE,
- state, btDeviceInfo, delay);
- }
+ sendILMsg(state == BluetoothA2dp.STATE_CONNECTED
+ ? MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED
+ : MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+ SENDMSG_QUEUE,
+ state, btDeviceInfo, delay);
}
/*package*/ void postA2dpSourceConnection(@AudioService.BtProfileConnectionState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo, int delay) {
- synchronized (mDeviceBrokerLock) {
- sendILMsg(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE,
- state, btDeviceInfo, delay);
- }
+ sendILMsg(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE,
+ state, btDeviceInfo, delay);
}
/*package*/ void postSetWiredDeviceConnectionState(
AudioDeviceInventory.WiredDeviceConnectionState connectionState, int delay) {
- synchronized (mDeviceBrokerLock) {
- sendLMsg(MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE, SENDMSG_QUEUE,
- connectionState, delay);
- }
+ sendLMsg(MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE, SENDMSG_QUEUE, connectionState, delay);
}
/*package*/ void postSetHearingAidConnectionState(
@AudioService.BtProfileConnectionState int state,
@NonNull BluetoothDevice device, int delay) {
- synchronized (mDeviceBrokerLock) {
- sendILMsg(MSG_IL_SET_HEARING_AID_CONNECTION_STATE, SENDMSG_QUEUE,
- state,
- device,
- delay);
- }
+ sendILMsg(MSG_IL_SET_HEARING_AID_CONNECTION_STATE, SENDMSG_QUEUE,
+ state,
+ device,
+ delay);
}
/*package*/ void postDisconnectA2dp() {
- synchronized (mDeviceBrokerLock) {
- sendMsgNoDelay(MSG_DISCONNECT_A2DP, SENDMSG_QUEUE);
- }
+ sendMsgNoDelay(MSG_DISCONNECT_A2DP, SENDMSG_QUEUE);
}
/*package*/ void postDisconnectA2dpSink() {
- synchronized (mDeviceBrokerLock) {
- sendMsgNoDelay(MSG_DISCONNECT_A2DP_SINK, SENDMSG_QUEUE);
- }
+ sendMsgNoDelay(MSG_DISCONNECT_A2DP_SINK, SENDMSG_QUEUE);
}
/*package*/ void postDisconnectHearingAid() {
- synchronized (mDeviceBrokerLock) {
- sendMsgNoDelay(MSG_DISCONNECT_BT_HEARING_AID, SENDMSG_QUEUE);
- }
+ sendMsgNoDelay(MSG_DISCONNECT_BT_HEARING_AID, SENDMSG_QUEUE);
}
/*package*/ void postDisconnectHeadset() {
- synchronized (mDeviceBrokerLock) {
- sendMsgNoDelay(MSG_DISCONNECT_BT_HEADSET, SENDMSG_QUEUE);
- }
+ sendMsgNoDelay(MSG_DISCONNECT_BT_HEADSET, SENDMSG_QUEUE);
}
/*package*/ void postBtA2dpProfileConnected(BluetoothA2dp a2dpProfile) {
- synchronized (mDeviceBrokerLock) {
- sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP, SENDMSG_QUEUE, a2dpProfile);
- }
+ sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP, SENDMSG_QUEUE, a2dpProfile);
}
/*package*/ void postBtA2dpSinkProfileConnected(BluetoothProfile profile) {
- synchronized (mDeviceBrokerLock) {
- sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK, SENDMSG_QUEUE, profile);
- }
+ sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK, SENDMSG_QUEUE, profile);
}
/*package*/ void postBtHeasetProfileConnected(BluetoothHeadset headsetProfile) {
- synchronized (mDeviceBrokerLock) {
- sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET, SENDMSG_QUEUE,
- headsetProfile);
- }
+ sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET, SENDMSG_QUEUE, headsetProfile);
}
/*package*/ void postBtHearingAidProfileConnected(BluetoothHearingAid hearingAidProfile) {
- synchronized (mDeviceBrokerLock) {
- sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID, SENDMSG_QUEUE,
- hearingAidProfile);
- }
+ sendLMsgNoDelay(MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID, SENDMSG_QUEUE,
+ hearingAidProfile);
}
/*package*/ void postScoClientDied(Object obj) {
- synchronized (mDeviceBrokerLock) {
- sendLMsgNoDelay(MSG_L_SCOCLIENT_DIED, SENDMSG_QUEUE, obj);
- }
+ sendLMsgNoDelay(MSG_L_SCOCLIENT_DIED, SENDMSG_QUEUE, obj);
}
//---------------------------------------------------------------------
@@ -570,7 +545,7 @@ import java.io.PrintWriter;
.append(") from u/pid:").append(Binder.getCallingUid()).append("/")
.append(Binder.getCallingPid()).append(" src:").append(source).toString();
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
mBluetoothA2dpEnabled = on;
mBrokerHandler.removeMessages(MSG_IIL_SET_FORCE_BT_A2DP_USE);
onSetForceUse(
@@ -582,85 +557,71 @@ import java.io.PrintWriter;
/*package*/ boolean handleDeviceConnection(boolean connect, int device, String address,
String deviceName) {
- return mDeviceInventory.handleDeviceConnection(connect, device, address, deviceName);
+ synchronized (mDeviceStateLock) {
+ return mDeviceInventory.handleDeviceConnection(connect, device, address, deviceName);
+ }
}
/*package*/ void postSetA2dpSourceConnectionState(@BluetoothProfile.BtProfileState int state,
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
final int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0;
- synchronized (mDeviceBrokerLock) {
- sendILMsgNoDelay(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE, state,
- btDeviceInfo);
- }
+ sendILMsgNoDelay(MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE, SENDMSG_QUEUE, state,
+ btDeviceInfo);
}
/*package*/ void handleFailureToConnectToBtHeadsetService(int delay) {
- synchronized (mDeviceBrokerLock) {
- sendMsg(MSG_BT_HEADSET_CNCT_FAILED, SENDMSG_REPLACE, delay);
- }
+ sendMsg(MSG_BT_HEADSET_CNCT_FAILED, SENDMSG_REPLACE, delay);
}
/*package*/ void handleCancelFailureToConnectToBtHeadsetService() {
- synchronized (mDeviceBrokerLock) {
- mBrokerHandler.removeMessages(MSG_BT_HEADSET_CNCT_FAILED);
- }
+ mBrokerHandler.removeMessages(MSG_BT_HEADSET_CNCT_FAILED);
}
/*package*/ void postReportNewRoutes() {
- synchronized (mDeviceBrokerLock) {
- sendMsgNoDelay(MSG_REPORT_NEW_ROUTES, SENDMSG_NOOP);
- }
+ sendMsgNoDelay(MSG_REPORT_NEW_ROUTES, SENDMSG_NOOP);
}
/*package*/ void cancelA2dpDockTimeout() {
- synchronized (mDeviceBrokerLock) {
- mBrokerHandler.removeMessages(MSG_IL_BTA2DP_DOCK_TIMEOUT);
- }
+ mBrokerHandler.removeMessages(MSG_IL_BTA2DP_DOCK_TIMEOUT);
}
- // FIXME: used by?
/*package*/ void postA2dpActiveDeviceChange(
@NonNull BtHelper.BluetoothA2dpDeviceInfo btDeviceInfo) {
- synchronized (mDeviceBrokerLock) {
- sendLMsgNoDelay(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE, SENDMSG_QUEUE, btDeviceInfo);
- }
+ sendLMsgNoDelay(MSG_L_A2DP_ACTIVE_DEVICE_CHANGE, SENDMSG_QUEUE, btDeviceInfo);
}
/*package*/ boolean hasScheduledA2dpDockTimeout() {
- synchronized (mDeviceBrokerLock) {
- return mBrokerHandler.hasMessages(MSG_IL_BTA2DP_DOCK_TIMEOUT);
- }
+ return mBrokerHandler.hasMessages(MSG_IL_BTA2DP_DOCK_TIMEOUT);
}
// must be called synchronized on mConnectedDevices
/*package*/ boolean hasScheduledA2dpSinkConnectionState(BluetoothDevice btDevice) {
- synchronized (mDeviceBrokerLock) {
- return (mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
- new BtHelper.BluetoothA2dpDeviceInfo(btDevice))
- || mBrokerHandler.hasMessages(
- MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
- new BtHelper.BluetoothA2dpDeviceInfo(btDevice)));
- }
+ return (mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED,
+ new BtHelper.BluetoothA2dpDeviceInfo(btDevice))
+ || mBrokerHandler.hasMessages(MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED,
+ new BtHelper.BluetoothA2dpDeviceInfo(btDevice)));
}
/*package*/ void setA2dpDockTimeout(String address, int a2dpCodec, int delayMs) {
- synchronized (mDeviceBrokerLock) {
- sendILMsg(MSG_IL_BTA2DP_DOCK_TIMEOUT, SENDMSG_QUEUE, a2dpCodec, address, delayMs);
- }
+ sendILMsg(MSG_IL_BTA2DP_DOCK_TIMEOUT, SENDMSG_QUEUE, a2dpCodec, address, delayMs);
}
/*package*/ void setAvrcpAbsoluteVolumeSupported(boolean supported) {
- mBtHelper.setAvrcpAbsoluteVolumeSupported(supported);
+ synchronized (mDeviceStateLock) {
+ mBtHelper.setAvrcpAbsoluteVolumeSupported(supported);
+ }
}
/*package*/ boolean getBluetoothA2dpEnabled() {
- synchronized (mDeviceBrokerLock) {
+ synchronized (mDeviceStateLock) {
return mBluetoothA2dpEnabled;
}
}
/*package*/ int getA2dpCodec(@NonNull BluetoothDevice device) {
- return mBtHelper.getA2dpCodec(device);
+ synchronized (mDeviceStateLock) {
+ return mBtHelper.getA2dpCodec(device);
+ }
}
/*package*/ void dump(PrintWriter pw, String prefix) {
@@ -748,101 +709,156 @@ import java.io.PrintWriter;
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_RESTORE_DEVICES:
- mDeviceInventory.onRestoreDevices();
- mBtHelper.onAudioServerDiedRestoreA2dp();
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onRestoreDevices();
+ mBtHelper.onAudioServerDiedRestoreA2dp();
+ }
break;
case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
- mDeviceInventory.onSetWiredDeviceConnectionState(
- (AudioDeviceInventory.WiredDeviceConnectionState) msg.obj);
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onSetWiredDeviceConnectionState(
+ (AudioDeviceInventory.WiredDeviceConnectionState) msg.obj);
+ }
break;
case MSG_I_BROADCAST_BT_CONNECTION_STATE:
- mBtHelper.onBroadcastScoConnectionState(msg.arg1);
+ synchronized (mDeviceStateLock) {
+ mBtHelper.onBroadcastScoConnectionState(msg.arg1);
+ }
break;
case MSG_IIL_SET_FORCE_USE: // intended fall-through
case MSG_IIL_SET_FORCE_BT_A2DP_USE:
onSetForceUse(msg.arg1, msg.arg2, (String) msg.obj);
break;
case MSG_REPORT_NEW_ROUTES:
- mDeviceInventory.onReportNewRoutes();
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onReportNewRoutes();
+ }
break;
case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
- mDeviceInventory.onSetA2dpSinkConnectionState(
- (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onSetA2dpSinkConnectionState(
+ (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
+ }
break;
case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
- mDeviceInventory.onSetA2dpSourceConnectionState(
- (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onSetA2dpSourceConnectionState(
+ (BtHelper.BluetoothA2dpDeviceInfo) msg.obj, msg.arg1);
+ }
break;
case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
- mDeviceInventory.onSetHearingAidConnectionState(
- (BluetoothDevice) msg.obj, msg.arg1,
- mAudioService.getHearingAidStreamType());
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onSetHearingAidConnectionState(
+ (BluetoothDevice) msg.obj, msg.arg1,
+ mAudioService.getHearingAidStreamType());
+ }
break;
case MSG_BT_HEADSET_CNCT_FAILED:
- mBtHelper.resetBluetoothSco();
+ synchronized (mSetModeLock) {
+ synchronized (mDeviceStateLock) {
+ mBtHelper.resetBluetoothSco();
+ }
+ }
break;
case MSG_IL_BTA2DP_DOCK_TIMEOUT:
// msg.obj == address of BTA2DP device
- mDeviceInventory.onMakeA2dpDeviceUnavailableNow((String) msg.obj, msg.arg1);
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onMakeA2dpDeviceUnavailableNow((String) msg.obj, msg.arg1);
+ }
break;
case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
final int a2dpCodec;
final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
- // FIXME why isn't the codec coming with the request? codec should be
- // provided by BT when it calls
- // AudioManager.handleBluetoothA2dpDeviceConfigChange(BluetoothDevice)
- a2dpCodec = mBtHelper.getA2dpCodec(btDevice);
- mDeviceInventory.onBluetoothA2dpActiveDeviceChange(
- new BtHelper.BluetoothA2dpDeviceInfo(btDevice, -1, a2dpCodec),
- BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
+ synchronized (mDeviceStateLock) {
+ a2dpCodec = mBtHelper.getA2dpCodec(btDevice);
+ mDeviceInventory.onBluetoothA2dpActiveDeviceChange(
+ new BtHelper.BluetoothA2dpDeviceInfo(btDevice, -1, a2dpCodec),
+ BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
+ }
break;
case MSG_BROADCAST_AUDIO_BECOMING_NOISY:
onSendBecomingNoisyIntent();
break;
case MSG_II_SET_HEARING_AID_VOLUME:
- mBtHelper.setHearingAidVolume(msg.arg1, msg.arg2);
+ synchronized (mDeviceStateLock) {
+ mBtHelper.setHearingAidVolume(msg.arg1, msg.arg2);
+ }
break;
case MSG_I_SET_AVRCP_ABSOLUTE_VOLUME:
- mBtHelper.setAvrcpAbsoluteVolumeIndex(msg.arg1);
+ synchronized (mDeviceStateLock) {
+ mBtHelper.setAvrcpAbsoluteVolumeIndex(msg.arg1);
+ }
break;
case MSG_I_DISCONNECT_BT_SCO:
- mBtHelper.disconnectBluetoothSco(msg.arg1);
+ synchronized (mSetModeLock) {
+ synchronized (mDeviceStateLock) {
+ mBtHelper.disconnectBluetoothSco(msg.arg1);
+ }
+ }
break;
case MSG_L_SCOCLIENT_DIED:
- mBtHelper.scoClientDied(msg.obj);
+ synchronized (mSetModeLock) {
+ synchronized (mDeviceStateLock) {
+ mBtHelper.scoClientDied(msg.obj);
+ }
+ }
break;
case MSG_TOGGLE_HDMI:
- mDeviceInventory.onToggleHdmi();
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onToggleHdmi();
+ }
break;
case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
- mDeviceInventory.onBluetoothA2dpActiveDeviceChange(
- (BtHelper.BluetoothA2dpDeviceInfo) msg.obj,
- BtHelper.EVENT_ACTIVE_DEVICE_CHANGE);
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.onBluetoothA2dpActiveDeviceChange(
+ (BtHelper.BluetoothA2dpDeviceInfo) msg.obj,
+ BtHelper.EVENT_ACTIVE_DEVICE_CHANGE);
+ }
break;
case MSG_DISCONNECT_A2DP:
- mDeviceInventory.disconnectA2dp();
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.disconnectA2dp();
+ }
break;
case MSG_DISCONNECT_A2DP_SINK:
- mDeviceInventory.disconnectA2dpSink();
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.disconnectA2dpSink();
+ }
break;
case MSG_DISCONNECT_BT_HEARING_AID:
- mDeviceInventory.disconnectHearingAid();
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.disconnectHearingAid();
+ }
break;
case MSG_DISCONNECT_BT_HEADSET:
- mBtHelper.disconnectHeadset();
+ synchronized (mSetModeLock) {
+ synchronized (mDeviceStateLock) {
+ mBtHelper.disconnectHeadset();
+ }
+ }
break;
case MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP:
- mBtHelper.onA2dpProfileConnected((BluetoothA2dp) msg.obj);
+ synchronized (mDeviceStateLock) {
+ mBtHelper.onA2dpProfileConnected((BluetoothA2dp) msg.obj);
+ }
break;
case MSG_L_BT_SERVICE_CONNECTED_PROFILE_A2DP_SINK:
- mBtHelper.onA2dpSinkProfileConnected((BluetoothProfile) msg.obj);
+ synchronized (mDeviceStateLock) {
+ mBtHelper.onA2dpSinkProfileConnected((BluetoothProfile) msg.obj);
+ }
break;
case MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEARING_AID:
- mBtHelper.onHearingAidProfileConnected((BluetoothHearingAid) msg.obj);
+ synchronized (mDeviceStateLock) {
+ mBtHelper.onHearingAidProfileConnected((BluetoothHearingAid) msg.obj);
+ }
break;
case MSG_L_BT_SERVICE_CONNECTED_PROFILE_HEADSET:
- mBtHelper.onHeadsetProfileConnected((BluetoothHeadset) msg.obj);
+ synchronized (mSetModeLock) {
+ synchronized (mDeviceStateLock) {
+ mBtHelper.onHeadsetProfileConnected((BluetoothHeadset) msg.obj);
+ }
+ }
break;
case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_CONNECTION:
case MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT_DISCONNECTION: {
@@ -855,9 +871,11 @@ import java.io.PrintWriter;
+ " addr=" + info.mDevice.getAddress()
+ " prof=" + info.mProfile + " supprNoisy=" + info.mSupprNoisy
+ " vol=" + info.mVolume)).printLog(TAG));
- mDeviceInventory.setBluetoothA2dpDeviceConnectionState(
- info.mDevice, info.mState, info.mProfile, info.mSupprNoisy,
- AudioSystem.DEVICE_NONE, info.mVolume);
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.setBluetoothA2dpDeviceConnectionState(
+ info.mDevice, info.mState, info.mProfile, info.mSupprNoisy,
+ AudioSystem.DEVICE_NONE, info.mVolume);
+ }
} break;
case MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT: {
final HearingAidDeviceConnectionInfo info =
@@ -867,8 +885,10 @@ import java.io.PrintWriter;
+ " addr=" + info.mDevice.getAddress()
+ " supprNoisy=" + info.mSupprNoisy
+ " src=" + info.mEventSource)).printLog(TAG));
- mDeviceInventory.setBluetoothHearingAidDeviceConnectionState(
- info.mDevice, info.mState, info.mSupprNoisy, info.mMusicDevice);
+ synchronized (mDeviceStateLock) {
+ mDeviceInventory.setBluetoothHearingAidDeviceConnectionState(
+ info.mDevice, info.mState, info.mSupprNoisy, info.mMusicDevice);
+ }
} break;
default:
Log.wtf(TAG, "Invalid message " + msg.what);
@@ -953,57 +973,46 @@ import java.io.PrintWriter;
/** If the msg is already queued, queue this one and leave the old. */
private static final int SENDMSG_QUEUE = 2;
- @GuardedBy("mDeviceBrokerLock")
private void sendMsg(int msg, int existingMsgPolicy, int delay) {
sendIILMsg(msg, existingMsgPolicy, 0, 0, null, delay);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendILMsg(int msg, int existingMsgPolicy, int arg, Object obj, int delay) {
sendIILMsg(msg, existingMsgPolicy, arg, 0, obj, delay);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendLMsg(int msg, int existingMsgPolicy, Object obj, int delay) {
sendIILMsg(msg, existingMsgPolicy, 0, 0, obj, delay);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendIMsg(int msg, int existingMsgPolicy, int arg, int delay) {
sendIILMsg(msg, existingMsgPolicy, arg, 0, null, delay);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendMsgNoDelay(int msg, int existingMsgPolicy) {
sendIILMsg(msg, existingMsgPolicy, 0, 0, null, 0);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendIMsgNoDelay(int msg, int existingMsgPolicy, int arg) {
sendIILMsg(msg, existingMsgPolicy, arg, 0, null, 0);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendIIMsgNoDelay(int msg, int existingMsgPolicy, int arg1, int arg2) {
sendIILMsg(msg, existingMsgPolicy, arg1, arg2, null, 0);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendILMsgNoDelay(int msg, int existingMsgPolicy, int arg, Object obj) {
sendIILMsg(msg, existingMsgPolicy, arg, 0, obj, 0);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendLMsgNoDelay(int msg, int existingMsgPolicy, Object obj) {
sendIILMsg(msg, existingMsgPolicy, 0, 0, obj, 0);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendIILMsgNoDelay(int msg, int existingMsgPolicy, int arg1, int arg2, Object obj) {
sendIILMsg(msg, existingMsgPolicy, arg1, arg2, obj, 0);
}
- @GuardedBy("mDeviceBrokerLock")
private void sendIILMsg(int msg, int existingMsgPolicy, int arg1, int arg2, Object obj,
int delay) {
if (existingMsgPolicy == SENDMSG_REPLACE) {
@@ -1022,29 +1031,31 @@ import java.io.PrintWriter;
Binder.restoreCallingIdentity(identity);
}
- long time = SystemClock.uptimeMillis() + delay;
+ synchronized (sLastDeviceConnectionMsgTimeLock) {
+ long time = SystemClock.uptimeMillis() + delay;
- switch (msg) {
- case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
- case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
- case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
- case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
- case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
- case MSG_IL_BTA2DP_DOCK_TIMEOUT:
- case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
- case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
- if (sLastDeviceConnectMsgTime >= time) {
- // add a little delay to make sure messages are ordered as expected
- time = sLastDeviceConnectMsgTime + 30;
- }
- sLastDeviceConnectMsgTime = time;
- break;
- default:
- break;
- }
+ switch (msg) {
+ case MSG_IL_SET_A2DP_SOURCE_CONNECTION_STATE:
+ case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_CONNECTED:
+ case MSG_IL_SET_A2DP_SINK_CONNECTION_STATE_DISCONNECTED:
+ case MSG_IL_SET_HEARING_AID_CONNECTION_STATE:
+ case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE:
+ case MSG_IL_BTA2DP_DOCK_TIMEOUT:
+ case MSG_L_A2DP_DEVICE_CONFIG_CHANGE:
+ case MSG_L_A2DP_ACTIVE_DEVICE_CHANGE:
+ if (sLastDeviceConnectMsgTime >= time) {
+ // add a little delay to make sure messages are ordered as expected
+ time = sLastDeviceConnectMsgTime + 30;
+ }
+ sLastDeviceConnectMsgTime = time;
+ break;
+ default:
+ break;
+ }
- mBrokerHandler.sendMessageAtTime(mBrokerHandler.obtainMessage(msg, arg1, arg2, obj),
- time);
+ mBrokerHandler.sendMessageAtTime(mBrokerHandler.obtainMessage(msg, arg1, arg2, obj),
+ time);
+ }
}
//-------------------------------------------------------------
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 3933fb2d5f46..90973a888a9d 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -159,6 +159,7 @@ public class AudioDeviceInventory {
}
// only public for mocking/spying
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
@VisibleForTesting
public void onSetA2dpSinkConnectionState(@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo,
@AudioService.BtProfileConnectionState int state) {
@@ -283,6 +284,7 @@ public class AudioDeviceInventory {
}
}
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ void onBluetoothA2dpActiveDeviceChange(
@NonNull BtHelper.BluetoothA2dpDeviceInfo btInfo, int event) {
final BluetoothDevice btDevice = btInfo.getBtDevice();
@@ -555,6 +557,7 @@ public class AudioDeviceInventory {
}
// only public for mocking/spying
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
@VisibleForTesting
public void setBluetoothA2dpDeviceConnectionState(
@NonNull BluetoothDevice device, @AudioService.BtProfileConnectionState int state,
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index cc50e37d2b61..0d493b825133 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -470,11 +470,12 @@ public class AudioService extends IAudioService.Stub
// List of binder death handlers for setMode() client processes.
// The last process to have called setMode() is at the top of the list.
- private final ArrayList<SetModeDeathHandler> mSetModeDeathHandlers =
+ // package-private so it can be accessed in AudioDeviceBroker.getSetModeDeathHandlers
+ //TODO candidate to be moved to separate class that handles synchronization
+ @GuardedBy("mDeviceBroker.mSetModeLock")
+ /*package*/ final ArrayList<SetModeDeathHandler> mSetModeDeathHandlers =
new ArrayList<SetModeDeathHandler>();
- private volatile int mCurrentModeOwnerPid = 0;
-
// true if boot sequence has been completed
private boolean mSystemReady;
// true if Intent.ACTION_USER_SWITCHED has ever been received
@@ -3191,10 +3192,15 @@ public class AudioService extends IAudioService.Stub
* @return 0 if nobody owns the mode
*/
/*package*/ int getModeOwnerPid() {
- return mCurrentModeOwnerPid;
+ int modeOwnerPid = 0;
+ try {
+ modeOwnerPid = mSetModeDeathHandlers.get(0).getPid();
+ } catch (Exception e) {
+ // nothing to do, modeOwnerPid is not modified
+ }
+ return modeOwnerPid;
}
-
private class SetModeDeathHandler implements IBinder.DeathRecipient {
private IBinder mCb; // To be notified of client's death
private int mPid;
@@ -3208,7 +3214,7 @@ public class AudioService extends IAudioService.Stub
public void binderDied() {
int oldModeOwnerPid = 0;
int newModeOwnerPid = 0;
- synchronized (mSetModeDeathHandlers) {
+ synchronized (mDeviceBroker.mSetModeLock) {
Log.w(TAG, "setMode() client died");
if (!mSetModeDeathHandlers.isEmpty()) {
oldModeOwnerPid = mSetModeDeathHandlers.get(0).getPid();
@@ -3219,15 +3225,11 @@ public class AudioService extends IAudioService.Stub
} else {
newModeOwnerPid = setModeInt(AudioSystem.MODE_NORMAL, mCb, mPid, TAG);
}
-
- if (newModeOwnerPid != oldModeOwnerPid) {
- mCurrentModeOwnerPid = newModeOwnerPid;
- // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
- // connections not started by the application changing the mode when pid changes
- if (newModeOwnerPid != 0) {
- mDeviceBroker.postDisconnectBluetoothSco(newModeOwnerPid);
- }
- }
+ }
+ // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all
+ // SCO connections not started by the application changing the mode when pid changes
+ if ((newModeOwnerPid != oldModeOwnerPid) && (newModeOwnerPid != 0)) {
+ mDeviceBroker.postDisconnectBluetoothSco(newModeOwnerPid);
}
}
@@ -3250,17 +3252,15 @@ public class AudioService extends IAudioService.Stub
/** @see AudioManager#setMode(int) */
public void setMode(int mode, IBinder cb, String callingPackage) {
- if (DEBUG_MODE) {
- Log.v(TAG, "setMode(mode=" + mode + ", callingPackage=" + callingPackage + ")");
- }
+ if (DEBUG_MODE) { Log.v(TAG, "setMode(mode=" + mode + ", callingPackage=" + callingPackage + ")"); }
if (!checkAudioSettingsPermission("setMode()")) {
return;
}
- if ((mode == AudioSystem.MODE_IN_CALL)
- && (mContext.checkCallingOrSelfPermission(
+ if ( (mode == AudioSystem.MODE_IN_CALL) &&
+ (mContext.checkCallingOrSelfPermission(
android.Manifest.permission.MODIFY_PHONE_STATE)
- != PackageManager.PERMISSION_GRANTED)) {
+ != PackageManager.PERMISSION_GRANTED)) {
Log.w(TAG, "MODIFY_PHONE_STATE Permission Denial: setMode(MODE_IN_CALL) from pid="
+ Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
return;
@@ -3272,7 +3272,7 @@ public class AudioService extends IAudioService.Stub
int oldModeOwnerPid = 0;
int newModeOwnerPid = 0;
- synchronized (mSetModeDeathHandlers) {
+ synchronized (mDeviceBroker.mSetModeLock) {
if (!mSetModeDeathHandlers.isEmpty()) {
oldModeOwnerPid = mSetModeDeathHandlers.get(0).getPid();
}
@@ -3280,21 +3280,17 @@ public class AudioService extends IAudioService.Stub
mode = mMode;
}
newModeOwnerPid = setModeInt(mode, cb, Binder.getCallingPid(), callingPackage);
-
- if (newModeOwnerPid != oldModeOwnerPid) {
- mCurrentModeOwnerPid = newModeOwnerPid;
- // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all
- // SCO connections not started by the application changing the mode when pid changes
- if (newModeOwnerPid != 0) {
- mDeviceBroker.postDisconnectBluetoothSco(newModeOwnerPid);
- }
- }
+ }
+ // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all
+ // SCO connections not started by the application changing the mode when pid changes
+ if ((newModeOwnerPid != oldModeOwnerPid) && (newModeOwnerPid != 0)) {
+ mDeviceBroker.postDisconnectBluetoothSco(newModeOwnerPid);
}
}
// setModeInt() returns a valid PID if the audio mode was successfully set to
// any mode other than NORMAL.
- @GuardedBy("mSetModeDeathHandlers")
+ @GuardedBy("mDeviceBroker.mSetModeLock")
private int setModeInt(int mode, IBinder cb, int pid, String caller) {
if (DEBUG_MODE) { Log.v(TAG, "setModeInt(mode=" + mode + ", pid=" + pid + ", caller="
+ caller + ")"); }
@@ -3633,7 +3629,9 @@ public class AudioService extends IAudioService.Stub
!mSystemReady) {
return;
}
- mDeviceBroker.startBluetoothScoForClient_Sync(cb, scoAudioMode, eventSource);
+ synchronized (mDeviceBroker.mSetModeLock) {
+ mDeviceBroker.startBluetoothScoForClient_Sync(cb, scoAudioMode, eventSource);
+ }
}
/** @see AudioManager#stopBluetoothSco() */
@@ -3645,7 +3643,9 @@ public class AudioService extends IAudioService.Stub
final String eventSource = new StringBuilder("stopBluetoothSco()")
.append(") from u/pid:").append(Binder.getCallingUid()).append("/")
.append(Binder.getCallingPid()).toString();
- mDeviceBroker.stopBluetoothScoForClient_Sync(cb, eventSource);
+ synchronized (mDeviceBroker.mSetModeLock) {
+ mDeviceBroker.stopBluetoothScoForClient_Sync(cb, eventSource);
+ }
}
@@ -4406,7 +4406,7 @@ public class AudioService extends IAudioService.Stub
// NOTE: Locking order for synchronized objects related to volume or ringer mode management:
// 1 mScoclient OR mSafeMediaVolumeState
- // 2 mSetModeDeathHandlers
+ // 2 mSetModeLock
// 3 mSettingsLock
// 4 VolumeStreamState.class
private class VolumeStreamState {
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 625b6b690443..9f1a6bd15ac3 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -171,6 +171,8 @@ public class BtHelper {
//----------------------------------------------------------------------
// Interface for AudioDeviceBroker
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void onSystemReady() {
mScoConnectionState = android.media.AudioManager.SCO_AUDIO_STATE_ERROR;
resetBluetoothSco();
@@ -243,6 +245,8 @@ public class BtHelper {
return mapBluetoothCodecToAudioFormat(btCodecConfig.getCodecType());
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void receiveBtEvent(Intent intent) {
final String action = intent.getAction();
if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
@@ -329,6 +333,8 @@ public class BtHelper {
*
* @param exceptPid pid whose SCO connections through {@link AudioManager} should be kept
*/
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void disconnectBluetoothSco(int exceptPid) {
checkScoAudioState();
if (mScoAudioState == SCO_STATE_ACTIVE_EXTERNAL) {
@@ -337,6 +343,8 @@ public class BtHelper {
clearAllScoClients(exceptPid, true);
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void startBluetoothScoForClient(IBinder cb, int scoAudioMode,
@NonNull String eventSource) {
ScoClient client = getScoClient(cb, true);
@@ -356,6 +364,8 @@ public class BtHelper {
Binder.restoreCallingIdentity(ident);
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void stopBluetoothScoForClient(IBinder cb,
@NonNull String eventSource) {
ScoClient client = getScoClient(cb, false);
@@ -413,6 +423,8 @@ public class BtHelper {
mDeviceBroker.postDisconnectHearingAid();
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void resetBluetoothSco() {
clearAllScoClients(0, false);
mScoAudioState = SCO_STATE_INACTIVE;
@@ -421,6 +433,8 @@ public class BtHelper {
mDeviceBroker.setBluetoothScoOn(false, "resetBluetoothSco");
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void disconnectHeadset() {
setBtScoActiveDevice(null);
mBluetoothHeadset = null;
@@ -466,6 +480,8 @@ public class BtHelper {
/*eventSource*/ "mBluetoothProfileServiceListener");
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void onHeadsetProfileConnected(BluetoothHeadset headset) {
// Discard timeout message
mDeviceBroker.handleCancelFailureToConnectToBtHeadsetService();
@@ -552,6 +568,8 @@ public class BtHelper {
return result;
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
@GuardedBy("BtHelper.this")
private void setBtScoActiveDevice(BluetoothDevice btDevice) {
Log.i(TAG, "setBtScoActiveDevice: " + mBluetoothHeadsetDevice + " -> " + btDevice);
@@ -634,6 +652,8 @@ public class BtHelper {
};
//----------------------------------------------------------------------
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
/*package*/ synchronized void scoClientDied(Object obj) {
final ScoClient client = (ScoClient) obj;
Log.w(TAG, "SCO client died");
@@ -664,6 +684,8 @@ public class BtHelper {
mDeviceBroker.postScoClientDied(this);
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ // @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
@GuardedBy("BtHelper.this")
void incCount(int scoAudioMode) {
if (!requestScoState(BluetoothHeadset.STATE_AUDIO_CONNECTED, scoAudioMode)) {
@@ -683,6 +705,8 @@ public class BtHelper {
mStartcount++;
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ // @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
@GuardedBy("BtHelper.this")
void decCount() {
if (mStartcount == 0) {
@@ -702,6 +726,8 @@ public class BtHelper {
}
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ // @GuardedBy("AudioDeviceBroker.mDeviceStateLock")
@GuardedBy("BtHelper.this")
void clearCount(boolean stopSco) {
if (mStartcount != 0) {
@@ -738,6 +764,8 @@ public class BtHelper {
return count;
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
@GuardedBy("BtHelper.this")
private boolean requestScoState(int state, int scoAudioMode) {
checkScoAudioState();
@@ -931,6 +959,8 @@ public class BtHelper {
return null;
}
+ // @GuardedBy("AudioDeviceBroker.mSetModeLock")
+ //@GuardedBy("AudioDeviceBroker.mDeviceStateLock")
@GuardedBy("BtHelper.this")
private void clearAllScoClients(int exceptPid, boolean stopSco) {
ScoClient savedClient = null;
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
new file mode 100644
index 000000000000..99b1ef4a1d2e
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
@@ -0,0 +1,300 @@
+/*
+ * 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.server.notification;
+
+import android.app.NotificationHistory;
+import android.app.NotificationHistory.HistoricalNotification;
+import android.os.Handler;
+import android.util.AtomicFile;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * Provides an interface to write and query for notification history data for a user from a Protocol
+ * Buffer database.
+ *
+ * Periodically writes the buffered history to disk but can also accept force writes based on
+ * outside changes (like a pending shutdown).
+ */
+public class NotificationHistoryDatabase {
+ private static final int DEFAULT_CURRENT_VERSION = 1;
+
+ private static final String TAG = "NotiHistoryDatabase";
+ private static final boolean DEBUG = NotificationManagerService.DBG;
+ private static final int HISTORY_RETENTION_DAYS = 2;
+ private static final long WRITE_BUFFER_INTERVAL_MS = 1000 * 60 * 20;
+
+ private final Object mLock = new Object();
+ private Handler mFileWriteHandler;
+ @VisibleForTesting
+ // List of files holding history information, sorted newest to oldest
+ final LinkedList<AtomicFile> mHistoryFiles;
+ private final GregorianCalendar mCal;
+ private final File mHistoryDir;
+ private final File mVersionFile;
+ // Current version of the database files schema
+ private int mCurrentVersion;
+ private final WriteBufferRunnable mWriteBufferRunnable;
+
+ // Object containing posted notifications that have not yet been written to disk
+ @VisibleForTesting
+ NotificationHistory mBuffer;
+
+ public NotificationHistoryDatabase(File dir) {
+ mCurrentVersion = DEFAULT_CURRENT_VERSION;
+ mVersionFile = new File(dir, "version");
+ mHistoryDir = new File(dir, "history");
+ mHistoryFiles = new LinkedList<>();
+ mCal = new GregorianCalendar();
+ mBuffer = new NotificationHistory();
+ mWriteBufferRunnable = new WriteBufferRunnable();
+ }
+
+ public void init(Handler fileWriteHandler) {
+ synchronized (mLock) {
+ mFileWriteHandler = fileWriteHandler;
+
+ try {
+ mHistoryDir.mkdir();
+ mVersionFile.createNewFile();
+ } catch (Exception e) {
+ Slog.e(TAG, "could not create needed files", e);
+ }
+
+ checkVersionAndBuildLocked();
+ indexFilesLocked();
+ prune(HISTORY_RETENTION_DAYS, System.currentTimeMillis());
+ }
+ }
+
+ private void indexFilesLocked() {
+ mHistoryFiles.clear();
+ final File[] files = mHistoryDir.listFiles();
+ if (files == null) {
+ return;
+ }
+
+ // Sort with newest files first
+ Arrays.sort(files, (lhs, rhs) -> Long.compare(rhs.lastModified(), lhs.lastModified()));
+
+ for (File file : files) {
+ mHistoryFiles.addLast(new AtomicFile(file));
+ }
+ }
+
+ private void checkVersionAndBuildLocked() {
+ int version;
+ try (BufferedReader reader = new BufferedReader(new FileReader(mVersionFile))) {
+ version = Integer.parseInt(reader.readLine());
+ } catch (NumberFormatException | IOException e) {
+ version = 0;
+ }
+
+ if (version != mCurrentVersion && mVersionFile.exists()) {
+ try (BufferedWriter writer = new BufferedWriter(new FileWriter(mVersionFile))) {
+ writer.write(Integer.toString(mCurrentVersion));
+ writer.write("\n");
+ writer.flush();
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to write new version");
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ void forceWriteToDisk() {
+ if (!mFileWriteHandler.hasCallbacks(mWriteBufferRunnable)) {
+ mFileWriteHandler.post(mWriteBufferRunnable);
+ }
+ }
+
+ void onPackageRemoved(String packageName) {
+ RemovePackageRunnable rpr = new RemovePackageRunnable(packageName);
+ mFileWriteHandler.post(rpr);
+ }
+
+ public void addNotification(final HistoricalNotification notification) {
+ synchronized (mLock) {
+ mBuffer.addNotificationToWrite(notification);
+ // Each time we have new history to write to disk, schedule a write in [interval] ms
+ if (mBuffer.getHistoryCount() == 1) {
+ mFileWriteHandler.postDelayed(mWriteBufferRunnable, WRITE_BUFFER_INTERVAL_MS);
+ }
+ }
+ }
+
+ public NotificationHistory readNotificationHistory() {
+ synchronized (mLock) {
+ NotificationHistory notifications = new NotificationHistory();
+
+ for (AtomicFile file : mHistoryFiles) {
+ try {
+ readLocked(
+ file, notifications, new NotificationHistoryFilter.Builder().build());
+ } catch (Exception e) {
+ Slog.e(TAG, "error reading " + file.getBaseFile().getName(), e);
+ }
+ }
+
+ return notifications;
+ }
+ }
+
+ public NotificationHistory readNotificationHistory(String packageName, String channelId,
+ int maxNotifications) {
+ synchronized (mLock) {
+ NotificationHistory notifications = new NotificationHistory();
+
+ for (AtomicFile file : mHistoryFiles) {
+ try {
+ readLocked(file, notifications,
+ new NotificationHistoryFilter.Builder()
+ .setPackage(packageName)
+ .setChannel(packageName, channelId)
+ .setMaxNotifications(maxNotifications)
+ .build());
+ if (maxNotifications == notifications.getHistoryCount()) {
+ // No need to read any more files
+ break;
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "error reading " + file.getBaseFile().getName(), e);
+ }
+ }
+
+ return notifications;
+ }
+ }
+
+ /**
+ * Remove any files that are too old.
+ */
+ public void prune(final int retentionDays, final long currentTimeMillis) {
+ synchronized (mLock) {
+ mCal.setTimeInMillis(currentTimeMillis);
+ mCal.add(Calendar.DATE, -1 * retentionDays);
+
+ while (!mHistoryFiles.isEmpty()) {
+ final AtomicFile currentOldestFile = mHistoryFiles.getLast();
+ final long age = currentTimeMillis
+ - currentOldestFile.getBaseFile().lastModified();
+ if (age > mCal.getTimeInMillis()) {
+ if (DEBUG) {
+ Slog.d(TAG, "Removed " + currentOldestFile.getBaseFile().getName());
+ }
+ currentOldestFile.delete();
+ mHistoryFiles.removeLast();
+ } else {
+ // all remaining files are newer than the cut off
+ return;
+ }
+ }
+ }
+ }
+
+ private void writeLocked(AtomicFile file, NotificationHistory notifications)
+ throws IOException {
+ FileOutputStream fos = file.startWrite();
+ try {
+ NotificationHistoryProtoHelper.write(fos, notifications, mCurrentVersion);
+ file.finishWrite(fos);
+ fos = null;
+ } finally {
+ // When fos is null (successful write), this will no-op
+ file.failWrite(fos);
+ }
+ }
+
+ private static void readLocked(AtomicFile file, NotificationHistory notificationsOut,
+ NotificationHistoryFilter filter) throws IOException {
+ try (FileInputStream in = file.openRead()) {
+ NotificationHistoryProtoHelper.read(in, notificationsOut, filter);
+ } catch (FileNotFoundException e) {
+ Slog.e(TAG, "Cannot file " + file.getBaseFile().getName(), e);
+ throw e;
+ }
+ }
+
+ private final class WriteBufferRunnable implements Runnable {
+ @Override
+ public void run() {
+ if (DEBUG) Slog.d(TAG, "WriteBufferRunnable");
+ synchronized (mLock) {
+ final AtomicFile latestNotificationsFiles = new AtomicFile(
+ new File(mHistoryDir, String.valueOf(System.currentTimeMillis())));
+ try {
+ writeLocked(latestNotificationsFiles, mBuffer);
+ mHistoryFiles.addFirst(latestNotificationsFiles);
+ mBuffer = new NotificationHistory();
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to write buffer to disk. not flushing buffer", e);
+ }
+ }
+ }
+ }
+
+ private final class RemovePackageRunnable implements Runnable {
+ private String mPkg;
+
+ public RemovePackageRunnable(String pkg) {
+ mPkg = pkg;
+ }
+
+ @Override
+ public void run() {
+ if (DEBUG) Slog.d(TAG, "RemovePackageRunnable");
+ synchronized (mLock) {
+ // Remove packageName entries from pending history
+ mBuffer.removeNotificationsFromWrite(mPkg);
+
+ // Remove packageName entries from files on disk, and rewrite them to disk
+ // Since we sort by modified date, we have to update the files oldest to newest to
+ // maintain the original ordering
+ Iterator<AtomicFile> historyFileItr = mHistoryFiles.descendingIterator();
+ while (historyFileItr.hasNext()) {
+ final AtomicFile af = historyFileItr.next();
+ try {
+ final NotificationHistory notifications = new NotificationHistory();
+ readLocked(af, notifications,
+ new NotificationHistoryFilter.Builder().build());
+ notifications.removeNotificationsFromWrite(mPkg);
+ writeLocked(af, notifications);
+ } catch (Exception e) {
+ Slog.e(TAG, "Cannot clean up file on pkg removal "
+ + af.getBaseFile().getName(), e);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryFilter.java b/services/core/java/com/android/server/notification/NotificationHistoryFilter.java
new file mode 100644
index 000000000000..c3b2e73b5354
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryFilter.java
@@ -0,0 +1,125 @@
+/*
+ * 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.server.notification;
+
+import android.annotation.NonNull;
+import android.app.NotificationHistory;
+import android.app.NotificationHistory.HistoricalNotification;
+import android.text.TextUtils;
+
+import com.android.internal.util.Preconditions;
+
+public final class NotificationHistoryFilter {
+ private String mPackage;
+ private String mChannel;
+ private int mNotificationCount;
+
+ private NotificationHistoryFilter() {}
+
+ public String getPackage() {
+ return mPackage;
+ }
+
+ public String getChannel() {
+ return mChannel;
+ }
+
+ public int getMaxNotifications() {
+ return mNotificationCount;
+ }
+
+ /**
+ * Returns whether any of the filtering conditions are set
+ */
+ public boolean isFiltering() {
+ return getPackage() != null || getChannel() != null
+ || mNotificationCount < Integer.MAX_VALUE;
+ }
+
+ /**
+ * Returns true if this notification passes the package and channel name filter, false
+ * otherwise.
+ */
+ public boolean matchesPackageAndChannelFilter(HistoricalNotification notification) {
+ if (!TextUtils.isEmpty(getPackage())) {
+ if (!getPackage().equals(notification.getPackage())) {
+ return false;
+ } else {
+ if (!TextUtils.isEmpty(getChannel())
+ && !getChannel().equals(notification.getChannelId())) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if the NotificationHistory can accept another notification.
+ */
+ public boolean matchesCountFilter(NotificationHistory notifications) {
+ return notifications.getHistoryCount() < mNotificationCount;
+ }
+
+ public static final class Builder {
+ private String mPackage = null;
+ private String mChannel = null;
+ private int mNotificationCount = Integer.MAX_VALUE;
+
+ /**
+ * Constructor
+ */
+ public Builder() {}
+
+ /**
+ * Sets a package name filter
+ */
+ public Builder setPackage(String aPackage) {
+ mPackage = aPackage;
+ return this;
+ }
+
+ /**
+ * Sets a channel name filter. Only valid if there is also a package name filter
+ */
+ public Builder setChannel(String pkg, String channel) {
+ setPackage(pkg);
+ mChannel = channel;
+ return this;
+ }
+
+ /**
+ * Sets the max historical notifications
+ */
+ public Builder setMaxNotifications(int notificationCount) {
+ mNotificationCount = notificationCount;
+ return this;
+ }
+
+ /**
+ * Makes a NotificationHistoryFilter
+ */
+ public NotificationHistoryFilter build() {
+ NotificationHistoryFilter filter = new NotificationHistoryFilter();
+ filter.mPackage = mPackage;
+ filter.mChannel = mChannel;
+ filter.mNotificationCount = mNotificationCount;
+ return filter;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java b/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java
new file mode 100644
index 000000000000..2831d37ed70b
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryProtoHelper.java
@@ -0,0 +1,321 @@
+/*
+ * 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.server.notification;
+
+import android.app.NotificationHistory;
+import android.app.NotificationHistory.HistoricalNotification;
+import android.content.res.Resources;
+import android.graphics.drawable.Icon;
+import android.util.Slog;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.server.notification.NotificationHistoryProto.Notification;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Notification history reader/writer for Protocol Buffer format
+ */
+final class NotificationHistoryProtoHelper {
+ private static final String TAG = "NotifHistoryProto";
+
+ // Static-only utility class.
+ private NotificationHistoryProtoHelper() {}
+
+ private static List<String> readStringPool(ProtoInputStream proto) throws IOException {
+ final long token = proto.start(NotificationHistoryProto.STRING_POOL);
+ List<String> stringPool;
+ if (proto.nextField(NotificationHistoryProto.StringPool.SIZE)) {
+ stringPool = new ArrayList(proto.readInt(NotificationHistoryProto.StringPool.SIZE));
+ } else {
+ stringPool = new ArrayList();
+ }
+ while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (proto.getFieldNumber()) {
+ case (int) NotificationHistoryProto.StringPool.STRINGS:
+ stringPool.add(proto.readString(NotificationHistoryProto.StringPool.STRINGS));
+ break;
+ }
+ }
+ proto.end(token);
+ return stringPool;
+ }
+
+ private static void writeStringPool(ProtoOutputStream proto,
+ final NotificationHistory notifications) {
+ final long token = proto.start(NotificationHistoryProto.STRING_POOL);
+ final String[] pooledStrings = notifications.getPooledStringsToWrite();
+ proto.write(NotificationHistoryProto.StringPool.SIZE, pooledStrings.length);
+ for (int i = 0; i < pooledStrings.length; i++) {
+ proto.write(NotificationHistoryProto.StringPool.STRINGS, pooledStrings[i]);
+ }
+ proto.end(token);
+ }
+
+ private static void readNotification(ProtoInputStream proto, List<String> stringPool,
+ NotificationHistory notifications, NotificationHistoryFilter filter)
+ throws IOException {
+ final long token = proto.start(NotificationHistoryProto.NOTIFICATION);
+ try {
+ HistoricalNotification notification = readNotification(proto, stringPool);
+ if (filter.matchesPackageAndChannelFilter(notification)
+ && filter.matchesCountFilter(notifications)) {
+ notifications.addNotificationToWrite(notification);
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Error reading notification", e);
+ } finally {
+ proto.end(token);
+ }
+ }
+
+ private static HistoricalNotification readNotification(ProtoInputStream parser,
+ List<String> stringPool) throws IOException {
+ final HistoricalNotification.Builder notification = new HistoricalNotification.Builder();
+ String pkg = null;
+ while (true) {
+ switch (parser.nextField()) {
+ case (int) NotificationHistoryProto.Notification.PACKAGE:
+ pkg = parser.readString(Notification.PACKAGE);
+ notification.setPackage(pkg);
+ stringPool.add(pkg);
+ break;
+ case (int) Notification.PACKAGE_INDEX:
+ pkg = stringPool.get(parser.readInt(Notification.PACKAGE_INDEX) - 1);
+ notification.setPackage(pkg);
+ break;
+ case (int) Notification.CHANNEL_NAME:
+ String channelName = parser.readString(Notification.CHANNEL_NAME);
+ notification.setChannelName(channelName);
+ stringPool.add(channelName);
+ break;
+ case (int) Notification.CHANNEL_NAME_INDEX:
+ notification.setChannelName(stringPool.get(parser.readInt(
+ Notification.CHANNEL_NAME_INDEX) - 1));
+ break;
+ case (int) Notification.CHANNEL_ID:
+ String channelId = parser.readString(Notification.CHANNEL_ID);
+ notification.setChannelId(channelId);
+ stringPool.add(channelId);
+ break;
+ case (int) Notification.CHANNEL_ID_INDEX:
+ notification.setChannelId(stringPool.get(parser.readInt(
+ Notification.CHANNEL_ID_INDEX) - 1));
+ break;
+ case (int) Notification.UID:
+ notification.setUid(parser.readInt(Notification.UID));
+ break;
+ case (int) Notification.USER_ID:
+ notification.setUserId(parser.readInt(Notification.USER_ID));
+ break;
+ case (int) Notification.POSTED_TIME_MS:
+ notification.setPostedTimeMs(parser.readLong(Notification.POSTED_TIME_MS));
+ break;
+ case (int) Notification.TITLE:
+ notification.setTitle(parser.readString(Notification.TITLE));
+ break;
+ case (int) Notification.TEXT:
+ notification.setText(parser.readString(Notification.TEXT));
+ break;
+ case (int) Notification.ICON:
+ final long iconToken = parser.start(Notification.ICON);
+ loadIcon(parser, notification, pkg);
+ parser.end(iconToken);
+ break;
+ case ProtoInputStream.NO_MORE_FIELDS:
+ return notification.build();
+ }
+ }
+ }
+
+ private static void loadIcon(ProtoInputStream parser,
+ HistoricalNotification.Builder notification, String pkg) throws IOException {
+ int iconType = Notification.TYPE_UNKNOWN;
+ String imageBitmapFileName = null;
+ int imageResourceId = Resources.ID_NULL;
+ String imageResourceIdPackage = null;
+ byte[] imageByteData = null;
+ int imageByteDataLength = 0;
+ int imageByteDataOffset = 0;
+ String imageUri = null;
+
+ while (true) {
+ switch (parser.nextField()) {
+ case (int) Notification.Icon.IMAGE_TYPE:
+ iconType = parser.readInt(Notification.Icon.IMAGE_TYPE);
+ break;
+ case (int) Notification.Icon.IMAGE_DATA:
+ imageByteData = parser.readBytes(Notification.Icon.IMAGE_DATA);
+ break;
+ case (int) Notification.Icon.IMAGE_DATA_LENGTH:
+ imageByteDataLength = parser.readInt(Notification.Icon.IMAGE_DATA_LENGTH);
+ break;
+ case (int) Notification.Icon.IMAGE_DATA_OFFSET:
+ imageByteDataOffset = parser.readInt(Notification.Icon.IMAGE_DATA_OFFSET);
+ break;
+ case (int) Notification.Icon.IMAGE_BITMAP_FILENAME:
+ imageBitmapFileName = parser.readString(
+ Notification.Icon.IMAGE_BITMAP_FILENAME);
+ break;
+ case (int) Notification.Icon.IMAGE_RESOURCE_ID:
+ imageResourceId = parser.readInt(Notification.Icon.IMAGE_RESOURCE_ID);
+ break;
+ case (int) Notification.Icon.IMAGE_RESOURCE_ID_PACKAGE:
+ imageResourceIdPackage = parser.readString(
+ Notification.Icon.IMAGE_RESOURCE_ID_PACKAGE);
+ break;
+ case (int) Notification.Icon.IMAGE_URI:
+ imageUri = parser.readString(Notification.Icon.IMAGE_URI);
+ break;
+ case ProtoInputStream.NO_MORE_FIELDS:
+ if (iconType == Icon.TYPE_DATA) {
+
+ if (imageByteData != null) {
+ notification.setIcon(Icon.createWithData(
+ imageByteData, imageByteDataOffset, imageByteDataLength));
+ }
+ } else if (iconType == Icon.TYPE_RESOURCE) {
+ if (imageResourceId != Resources.ID_NULL) {
+ notification.setIcon(Icon.createWithResource(
+ imageResourceIdPackage != null
+ ? imageResourceIdPackage
+ : pkg,
+ imageResourceId));
+ }
+ } else if (iconType == Icon.TYPE_URI) {
+ if (imageUri != null) {
+ notification.setIcon(Icon.createWithContentUri(imageUri));
+ }
+ } else if (iconType == Icon.TYPE_BITMAP) {
+ // TODO: read file from disk
+ }
+ return;
+ }
+ }
+ }
+
+ private static void writeIcon(ProtoOutputStream proto, HistoricalNotification notification) {
+ final long token = proto.start(Notification.ICON);
+
+ proto.write(Notification.Icon.IMAGE_TYPE, notification.getIcon().getType());
+ switch (notification.getIcon().getType()) {
+ case Icon.TYPE_DATA:
+ proto.write(Notification.Icon.IMAGE_DATA, notification.getIcon().getDataBytes());
+ proto.write(Notification.Icon.IMAGE_DATA_LENGTH,
+ notification.getIcon().getDataLength());
+ proto.write(Notification.Icon.IMAGE_DATA_OFFSET,
+ notification.getIcon().getDataOffset());
+ break;
+ case Icon.TYPE_RESOURCE:
+ proto.write(Notification.Icon.IMAGE_RESOURCE_ID, notification.getIcon().getResId());
+ if (!notification.getPackage().equals(notification.getIcon().getResPackage())) {
+ proto.write(Notification.Icon.IMAGE_RESOURCE_ID_PACKAGE,
+ notification.getIcon().getResPackage());
+ }
+ break;
+ case Icon.TYPE_URI:
+ proto.write(Notification.Icon.IMAGE_URI, notification.getIcon().getUriString());
+ break;
+ case Icon.TYPE_BITMAP:
+ // TODO: write file to disk
+ break;
+ }
+
+ proto.end(token);
+ }
+
+ private static void writeNotification(ProtoOutputStream proto,
+ final String[] stringPool, final HistoricalNotification notification) {
+ final long token = proto.start(NotificationHistoryProto.NOTIFICATION);
+ final int packageIndex = Arrays.binarySearch(stringPool, notification.getPackage());
+ if (packageIndex >= 0) {
+ proto.write(Notification.PACKAGE_INDEX, packageIndex + 1);
+ } else {
+ // Package not in Stringpool for some reason, write full string instead
+ Slog.w(TAG, "notification package name (" + notification.getPackage()
+ + ") not found in string cache");
+ proto.write(Notification.PACKAGE, notification.getPackage());
+ }
+ final int channelNameIndex = Arrays.binarySearch(stringPool, notification.getChannelName());
+ if (channelNameIndex >= 0) {
+ proto.write(Notification.CHANNEL_NAME_INDEX, channelNameIndex + 1);
+ } else {
+ Slog.w(TAG, "notification channel name (" + notification.getChannelName()
+ + ") not found in string cache");
+ proto.write(Notification.CHANNEL_NAME, notification.getChannelName());
+ }
+ final int channelIdIndex = Arrays.binarySearch(stringPool, notification.getChannelId());
+ if (channelIdIndex >= 0) {
+ proto.write(Notification.CHANNEL_ID_INDEX, channelIdIndex + 1);
+ } else {
+ Slog.w(TAG, "notification channel id (" + notification.getChannelId()
+ + ") not found in string cache");
+ proto.write(Notification.CHANNEL_ID, notification.getChannelId());
+ }
+ proto.write(Notification.UID, notification.getUid());
+ proto.write(Notification.USER_ID, notification.getUserId());
+ proto.write(Notification.POSTED_TIME_MS, notification.getPostedTimeMs());
+ proto.write(Notification.TITLE, notification.getTitle());
+ proto.write(Notification.TEXT, notification.getText());
+ writeIcon(proto, notification);
+ proto.end(token);
+ }
+
+ public static void read(InputStream in, NotificationHistory notifications,
+ NotificationHistoryFilter filter) throws IOException {
+ final ProtoInputStream proto = new ProtoInputStream(in);
+ List<String> stringPool = new ArrayList<>();
+ while (true) {
+ switch (proto.nextField()) {
+ case (int) NotificationHistoryProto.STRING_POOL:
+ stringPool = readStringPool(proto);
+ break;
+ case (int) NotificationHistoryProto.NOTIFICATION:
+ readNotification(proto, stringPool, notifications, filter);
+ break;
+ case ProtoInputStream.NO_MORE_FIELDS:
+ if (filter.isFiltering()) {
+ notifications.poolStringsFromNotifications();
+ } else {
+ notifications.addPooledStrings(stringPool);
+ }
+ return;
+ }
+ }
+ }
+
+ public static void write(OutputStream out, NotificationHistory notifications, int version) {
+ final ProtoOutputStream proto = new ProtoOutputStream(out);
+ proto.write(NotificationHistoryProto.MAJOR_VERSION, version);
+ // String pool should be written before the history itself
+ writeStringPool(proto, notifications);
+
+ List<HistoricalNotification> notificationsToWrite = notifications.getNotificationsToWrite();
+ final int count = notificationsToWrite.size();
+ for (int i = 0; i < count; i++) {
+ writeNotification(proto, notifications.getPooledStringsToWrite(),
+ notificationsToWrite.get(i));
+ }
+
+ proto.flush();
+ }
+}
diff --git a/services/core/java/com/android/server/pm/AppsFilter.java b/services/core/java/com/android/server/pm/AppsFilter.java
index c8179a767d23..dc0cd184c188 100644
--- a/services/core/java/com/android/server/pm/AppsFilter.java
+++ b/services/core/java/com/android/server/pm/AppsFilter.java
@@ -41,7 +41,6 @@ import android.util.SparseArray;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.FgThread;
-import com.android.server.compat.CompatConfig;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -131,11 +130,11 @@ public class AppsFilter {
private static class FeatureConfigImpl implements FeatureConfig {
private static final String FILTERING_ENABLED_NAME = "package_query_filtering_enabled";
+ private final PackageManagerService.Injector mInjector;
private volatile boolean mFeatureEnabled = false;
- private CompatConfig mCompatibility;
private FeatureConfigImpl(PackageManagerService.Injector injector) {
- mCompatibility = injector.getCompatibility();
+ mInjector = injector;
}
@Override
@@ -158,7 +157,7 @@ public class AppsFilter {
@Override
public boolean packageIsEnabled(PackageParser.Package pkg) {
- return mCompatibility.isChangeEnabled(
+ return mInjector.getCompatibility().isChangeEnabled(
PackageManager.FILTER_APPLICATION_QUERY, pkg.applicationInfo);
}
}
@@ -263,10 +262,10 @@ public class AppsFilter {
* Grants access based on an interaction between a calling and target package, granting
* visibility of the caller from the target.
*
- * @param callingPackage the package initiating the interaction
- * @param targetPackage the package being interacted with and thus gaining visibility of the
- * initiating package.
- * @param userId the user in which this interaction was taking place
+ * @param callingPackage the package initiating the interaction
+ * @param targetPackage the package being interacted with and thus gaining visibility of the
+ * initiating package.
+ * @param userId the user in which this interaction was taking place
*/
public void grantImplicitAccess(
String callingPackage, String targetPackage, int userId) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 74a85d58c016..b36958a69162 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -299,7 +299,7 @@ import com.android.server.ServiceThread;
import com.android.server.SystemConfig;
import com.android.server.SystemServerInitThreadPool;
import com.android.server.Watchdog;
-import com.android.server.compat.CompatConfig;
+import com.android.server.compat.PlatformCompat;
import com.android.server.net.NetworkPolicyManagerInternal;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.Settings.DatabaseVersion;
@@ -837,7 +837,7 @@ public class PackageManagerService extends IPackageManager.Stub
private final Singleton<StorageManager> mStorageManagerProducer;
private final Singleton<AppOpsManager> mAppOpsManagerProducer;
private final Singleton<AppsFilter> mAppsFilterProducer;
- private final Singleton<CompatConfig> mPlatformCompatProducer;
+ private final Singleton<PlatformCompat> mPlatformCompatProducer;
Injector(Context context, Object lock, Installer installer,
Object installLock, PackageAbiHelper abiHelper,
@@ -855,7 +855,7 @@ public class PackageManagerService extends IPackageManager.Stub
Producer<StorageManager> storageManagerProducer,
Producer<AppOpsManager> appOpsManagerProducer,
Producer<AppsFilter> appsFilterProducer,
- Producer<CompatConfig> platformCompatProducer) {
+ Producer<PlatformCompat> platformCompatProducer) {
mContext = context;
mLock = lock;
mInstaller = installer;
@@ -966,7 +966,7 @@ public class PackageManagerService extends IPackageManager.Stub
return mAppsFilterProducer.get(this, mPackageManager);
}
- public CompatConfig getCompatibility() {
+ public PlatformCompat getCompatibility() {
return mPlatformCompatProducer.get(this, mPackageManager);
}
}
@@ -2356,7 +2356,7 @@ public class PackageManagerService extends IPackageManager.Stub
new Injector.SystemServiceProducer<>(StorageManager.class),
new Injector.SystemServiceProducer<>(AppOpsManager.class),
(i, pm) -> AppsFilter.create(i),
- (i, pm) -> CompatConfig.get());
+ (i, pm) -> (PlatformCompat) ServiceManager.getService("platform_compat"));
PackageManagerService m = new PackageManagerService(injector, factoryTest, onlyCore);
t.traceEnd(); // "create package manager"
diff --git a/services/core/jni/com_android_server_VibratorService.cpp b/services/core/jni/com_android_server_VibratorService.cpp
index a8c76827c43c..64c7935efff9 100644
--- a/services/core/jni/com_android_server_VibratorService.cpp
+++ b/services/core/jni/com_android_server_VibratorService.cpp
@@ -190,7 +190,7 @@ static void vibratorSetExternalControl(JNIEnv*, jclass, jboolean enabled) {
}
}
-static jlong vibratorPerformEffect(JNIEnv* env, jclass, jlong effect, jint strength,
+static jlong vibratorPerformEffect(JNIEnv* env, jclass, jlong effect, jlong strength,
jobject vibration) {
Status status;
uint32_t lengthMs;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
index 29a8dada7d89..5c2ad94720f0 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -126,22 +126,6 @@ public class AudioDeviceBrokerTest {
doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2);
}
- /**
- * Verify connecting an A2DP sink will call into AudioService to unmute media
- */
- @Test
- public void testA2dpConnectionUnmutesMedia() throws Exception {
- Log.i(TAG, "testA2dpConnectionUnmutesMedia");
- Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
-
- mAudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(mFakeBtDevice,
- BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, true, 1);
- Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
- verify(mMockAudioService, times(1)).postAccessoryPlugMediaUnmute(
- ArgumentMatchers.eq(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
-
- }
-
private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection)
throws Exception {
when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC))
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
new file mode 100644
index 000000000000..bcff2f81f805
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.server.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.NotificationHistory.HistoricalNotification;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+import android.util.AtomicFile;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationHistoryDatabaseTest extends UiServiceTestCase {
+
+ File mRootDir;
+ @Mock
+ Handler mFileWriteHandler;
+
+ NotificationHistoryDatabase mDataBase;
+
+ private HistoricalNotification getHistoricalNotification(int index) {
+ return getHistoricalNotification("package" + index, index);
+ }
+
+ private HistoricalNotification getHistoricalNotification(String packageName, int index) {
+ String expectedChannelName = "channelName" + index;
+ String expectedChannelId = "channelId" + index;
+ int expectedUid = 1123456 + index;
+ int expectedUserId = 11 + index;
+ long expectedPostTime = 987654321 + index;
+ String expectedTitle = "title" + index;
+ String expectedText = "text" + index;
+ Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
+ index);
+
+ return new HistoricalNotification.Builder()
+ .setPackage(packageName)
+ .setChannelName(expectedChannelName)
+ .setChannelId(expectedChannelId)
+ .setUid(expectedUid)
+ .setUserId(expectedUserId)
+ .setPostedTimeMs(expectedPostTime)
+ .setTitle(expectedTitle)
+ .setText(expectedText)
+ .setIcon(expectedIcon)
+ .build();
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest");
+
+ mDataBase = new NotificationHistoryDatabase(mRootDir);
+ mDataBase.init(mFileWriteHandler);
+ }
+
+ @Test
+ public void testPrune() {
+ int retainDays = 1;
+ for (long i = 10; i >= 5; i--) {
+ File file = mock(File.class);
+ when(file.lastModified()).thenReturn(i);
+ AtomicFile af = new AtomicFile(file);
+ mDataBase.mHistoryFiles.addLast(af);
+ }
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTimeInMillis(5);
+ cal.add(Calendar.DATE, -1 * retainDays);
+ for (int i = 5; i >= 0; i--) {
+ File file = mock(File.class);
+ when(file.lastModified()).thenReturn(cal.getTimeInMillis() - i);
+ AtomicFile af = new AtomicFile(file);
+ mDataBase.mHistoryFiles.addLast(af);
+ }
+ mDataBase.prune(retainDays, 10);
+
+ for (AtomicFile file : mDataBase.mHistoryFiles) {
+ assertThat(file.getBaseFile().lastModified() > 0);
+ }
+ }
+
+ @Test
+ public void testOnPackageRemove_posts() {
+ mDataBase.onPackageRemoved("test");
+ verify(mFileWriteHandler, times(1)).post(any());
+ }
+
+ @Test
+ public void testForceWriteToDisk() {
+ mDataBase.forceWriteToDisk();
+ verify(mFileWriteHandler, times(1)).post(any());
+ }
+
+ @Test
+ public void testOnlyOneWriteRunnableInQueue() {
+ when(mFileWriteHandler.hasCallbacks(any())).thenReturn(true);
+ mDataBase.forceWriteToDisk();
+ verify(mFileWriteHandler, never()).post(any());
+ }
+
+ @Test
+ public void testAddNotification() {
+ HistoricalNotification n = getHistoricalNotification(1);
+ HistoricalNotification n2 = getHistoricalNotification(2);
+
+ mDataBase.addNotification(n);
+ assertThat(mDataBase.mBuffer.getNotificationsToWrite()).contains(n);
+ verify(mFileWriteHandler, times(1)).postDelayed(any(), anyLong());
+
+ // second add should not trigger another write
+ mDataBase.addNotification(n2);
+ assertThat(mDataBase.mBuffer.getNotificationsToWrite()).contains(n2);
+ verify(mFileWriteHandler, times(1)).postDelayed(any(), anyLong());
+ }
+
+ @Test
+ public void testReadNotificationHistory_readsAllFiles() throws Exception {
+ for (long i = 10; i >= 5; i--) {
+ AtomicFile af = mock(AtomicFile.class);
+ mDataBase.mHistoryFiles.addLast(af);
+ }
+
+ mDataBase.readNotificationHistory();
+
+ for (AtomicFile file : mDataBase.mHistoryFiles) {
+ verify(file, times(1)).openRead();
+ }
+ }
+
+ @Test
+ public void testReadNotificationHistory_withNumFilterDoesNotReadExtraFiles() throws Exception {
+ AtomicFile af = mock(AtomicFile.class);
+ when(af.getBaseFile()).thenReturn(new File(mRootDir, "af"));
+ mDataBase.mHistoryFiles.addLast(af);
+
+ AtomicFile af2 = mock(AtomicFile.class);
+ when(af2.getBaseFile()).thenReturn(new File(mRootDir, "af2"));
+ mDataBase.mHistoryFiles.addLast(af2);
+
+ mDataBase.readNotificationHistory(null, null, 0);
+
+ verify(af, times(1)).openRead();
+ verify(af2, never()).openRead();
+ }
+
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryFilterTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryFilterTest.java
new file mode 100644
index 000000000000..10bfcf12c89f
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryFilterTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.server.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.NotificationHistory;
+import android.app.NotificationHistory.HistoricalNotification;
+import android.graphics.drawable.Icon;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NotificationHistoryFilterTest extends UiServiceTestCase {
+
+ private HistoricalNotification getHistoricalNotification(int index) {
+ return getHistoricalNotification("package" + index, "channelId" + index, index);
+ }
+ private HistoricalNotification getHistoricalNotification(String pkg, int index) {
+ return getHistoricalNotification(pkg, "channelId" + index, index);
+ }
+
+ private HistoricalNotification getHistoricalNotification(String packageName, String channelId,
+ int index) {
+ String expectedChannelName = "channelName" + index;
+ int expectedUid = 1123456 + index;
+ int expectedUserId = 11 + index;
+ long expectedPostTime = 987654321 + index;
+ String expectedTitle = "title" + index;
+ String expectedText = "text" + index;
+ Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
+ index);
+
+ return new HistoricalNotification.Builder()
+ .setPackage(packageName)
+ .setChannelName(expectedChannelName)
+ .setChannelId(channelId)
+ .setUid(expectedUid)
+ .setUserId(expectedUserId)
+ .setPostedTimeMs(expectedPostTime)
+ .setTitle(expectedTitle)
+ .setText(expectedText)
+ .setIcon(expectedIcon)
+ .build();
+ }
+
+ @Test
+ public void testBuilder() {
+ NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
+ .setChannel("pkg", "channel")
+ .setMaxNotifications(3)
+ .build();
+
+ assertThat(filter.getPackage()).isEqualTo("pkg");
+ assertThat(filter.getChannel()).isEqualTo("channel");
+ assertThat(filter.getMaxNotifications()).isEqualTo(3);
+ }
+
+ @Test
+ public void testMatchesCountFilter() {
+ NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
+ .setMaxNotifications(3)
+ .build();
+
+ NotificationHistory history = new NotificationHistory();
+ assertThat(filter.matchesCountFilter(history)).isTrue();
+ history.addNotificationToWrite(getHistoricalNotification(1));
+ assertThat(filter.matchesCountFilter(history)).isTrue();
+ history.addNotificationToWrite(getHistoricalNotification(2));
+ assertThat(filter.matchesCountFilter(history)).isTrue();
+ history.addNotificationToWrite(getHistoricalNotification(3));
+ assertThat(filter.matchesCountFilter(history)).isFalse();
+ }
+
+ @Test
+ public void testMatchesCountFilter_noCountFilter() {
+ NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
+ .build();
+
+ NotificationHistory history = new NotificationHistory();
+ assertThat(filter.matchesCountFilter(history)).isTrue();
+ history.addNotificationToWrite(getHistoricalNotification(1));
+ assertThat(filter.matchesCountFilter(history)).isTrue();
+ }
+
+ @Test
+ public void testMatchesPackageAndChannelFilter_pkgOnly() {
+ NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
+ .setPackage("pkg")
+ .build();
+
+ HistoricalNotification hnMatches = getHistoricalNotification("pkg", 1);
+ assertThat(filter.matchesPackageAndChannelFilter(hnMatches)).isTrue();
+ HistoricalNotification hnMatches2 = getHistoricalNotification("pkg", 2);
+ assertThat(filter.matchesPackageAndChannelFilter(hnMatches2)).isTrue();
+
+ HistoricalNotification hnNoMatch = getHistoricalNotification("pkg2", 2);
+ assertThat(filter.matchesPackageAndChannelFilter(hnNoMatch)).isFalse();
+ }
+
+ @Test
+ public void testMatchesPackageAndChannelFilter_channelAlso() {
+ NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
+ .setChannel("pkg", "channel")
+ .build();
+
+ HistoricalNotification hn1 = getHistoricalNotification("pkg", 1);
+ assertThat(filter.matchesPackageAndChannelFilter(hn1)).isFalse();
+
+ HistoricalNotification hn2 = getHistoricalNotification("pkg", "channel", 1);
+ assertThat(filter.matchesPackageAndChannelFilter(hn2)).isTrue();
+
+ HistoricalNotification hn3 = getHistoricalNotification("pkg2", "channel", 1);
+ assertThat(filter.matchesPackageAndChannelFilter(hn3)).isFalse();
+ }
+
+ @Test
+ public void testIsFiltering() {
+ NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
+ .build();
+ assertThat(filter.isFiltering()).isFalse();
+
+ filter = new NotificationHistoryFilter.Builder()
+ .setPackage("pkg")
+ .build();
+ assertThat(filter.isFiltering()).isTrue();
+
+ filter = new NotificationHistoryFilter.Builder()
+ .setChannel("pkg", "channel")
+ .build();
+ assertThat(filter.isFiltering()).isTrue();
+
+ filter = new NotificationHistoryFilter.Builder()
+ .setMaxNotifications(5)
+ .build();
+ assertThat(filter.isFiltering()).isTrue();
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryProtoHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryProtoHelperTest.java
new file mode 100644
index 000000000000..458117d50784
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryProtoHelperTest.java
@@ -0,0 +1,297 @@
+/*
+ * 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.server.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.NotificationHistory;
+import android.app.NotificationHistory.HistoricalNotification;
+import android.graphics.drawable.Icon;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NotificationHistoryProtoHelperTest extends UiServiceTestCase {
+
+ private HistoricalNotification getHistoricalNotification(int index) {
+ return getHistoricalNotification("package" + index, index);
+ }
+
+ private HistoricalNotification getHistoricalNotification(String packageName, int index) {
+ String expectedChannelName = "channelName" + index;
+ String expectedChannelId = "channelId" + index;
+ int expectedUid = 1123456 + index;
+ int expectedUserId = 11 + index;
+ long expectedPostTime = 987654321 + index;
+ String expectedTitle = "title" + index;
+ String expectedText = "text" + index;
+ Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
+ index);
+
+ return new HistoricalNotification.Builder()
+ .setPackage(packageName)
+ .setChannelName(expectedChannelName)
+ .setChannelId(expectedChannelId)
+ .setUid(expectedUid)
+ .setUserId(expectedUserId)
+ .setPostedTimeMs(expectedPostTime)
+ .setTitle(expectedTitle)
+ .setText(expectedText)
+ .setIcon(expectedIcon)
+ .build();
+ }
+
+ @Test
+ public void testReadWriteNotifications() throws Exception {
+ NotificationHistory history = new NotificationHistory();
+
+ List<HistoricalNotification> expectedEntries = new ArrayList<>();
+ // loops backwards just to maintain the post time newest -> oldest expectation
+ for (int i = 10; i >= 1; i--) {
+ HistoricalNotification n = getHistoricalNotification(i);
+ expectedEntries.add(n);
+ history.addNotificationToWrite(n);
+ }
+ history.poolStringsFromNotifications();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ NotificationHistoryProtoHelper.write(baos, history, 1);
+
+ NotificationHistory actualHistory = new NotificationHistory();
+ NotificationHistoryProtoHelper.read(
+ new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
+ actualHistory,
+ new NotificationHistoryFilter.Builder().build());
+
+ assertThat(actualHistory.getHistoryCount()).isEqualTo(history.getHistoryCount());
+ assertThat(actualHistory.getNotificationsToWrite())
+ .containsExactlyElementsIn(expectedEntries);
+ }
+
+ @Test
+ public void testReadWriteNotifications_stringFieldsPersistedEvenIfNoPool() throws Exception {
+ NotificationHistory history = new NotificationHistory();
+
+ List<HistoricalNotification> expectedEntries = new ArrayList<>();
+ // loops backwards just to maintain the post time newest -> oldest expectation
+ for (int i = 10; i >= 1; i--) {
+ HistoricalNotification n = getHistoricalNotification(i);
+ expectedEntries.add(n);
+ history.addNotificationToWrite(n);
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ NotificationHistoryProtoHelper.write(baos, history, 1);
+
+ NotificationHistory actualHistory = new NotificationHistory();
+ NotificationHistoryProtoHelper.read(
+ new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
+ actualHistory,
+ new NotificationHistoryFilter.Builder().build());
+
+ assertThat(actualHistory.getHistoryCount()).isEqualTo(history.getHistoryCount());
+ assertThat(actualHistory.getNotificationsToWrite())
+ .containsExactlyElementsIn(expectedEntries);
+ }
+
+ @Test
+ public void testReadNotificationsWithPkgFilter() throws Exception {
+ NotificationHistory history = new NotificationHistory();
+
+ List<HistoricalNotification> expectedEntries = new ArrayList<>();
+ Set<String> expectedStrings = new HashSet<>();
+ // loops backwards just to maintain the post time newest -> oldest expectation
+ for (int i = 10; i >= 1; i--) {
+ HistoricalNotification n =
+ getHistoricalNotification((i % 2 == 0) ? "pkgEven" : "pkgOdd", i);
+
+ if (i % 2 == 0) {
+ expectedStrings.add(n.getPackage());
+ expectedStrings.add(n.getChannelName());
+ expectedStrings.add(n.getChannelId());
+ expectedEntries.add(n);
+ }
+ history.addNotificationToWrite(n);
+ }
+ history.poolStringsFromNotifications();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ NotificationHistoryProtoHelper.write(baos, history, 1);
+
+ NotificationHistory actualHistory = new NotificationHistory();
+
+ NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
+ .setPackage("pkgEven")
+ .build();
+ NotificationHistoryProtoHelper.read(
+ new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
+ actualHistory,
+ filter);
+
+ assertThat(actualHistory.getNotificationsToWrite())
+ .containsExactlyElementsIn(expectedEntries);
+ assertThat(Arrays.asList(actualHistory.getPooledStringsToWrite()))
+ .containsExactlyElementsIn(expectedStrings);
+ }
+
+ @Test
+ public void testReadNotificationsWithNumberFilter() throws Exception {
+ int maxCount = 3;
+ NotificationHistory history = new NotificationHistory();
+
+ List<HistoricalNotification> expectedEntries = new ArrayList<>();
+ Set<String> expectedStrings = new HashSet<>();
+ for (int i = 1; i < 10; i++) {
+ HistoricalNotification n = getHistoricalNotification(i);
+
+ if (i <= maxCount) {
+ expectedStrings.add(n.getPackage());
+ expectedStrings.add(n.getChannelName());
+ expectedStrings.add(n.getChannelId());
+ expectedEntries.add(n);
+ }
+ history.addNotificationToWrite(n);
+ }
+ history.poolStringsFromNotifications();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ NotificationHistoryProtoHelper.write(baos, history, 1);
+
+ NotificationHistory actualHistory = new NotificationHistory();
+
+ NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
+ .setMaxNotifications(maxCount)
+ .build();
+ NotificationHistoryProtoHelper.read(
+ new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
+ actualHistory,
+ filter);
+
+ assertThat(actualHistory.getNotificationsToWrite())
+ .containsExactlyElementsIn(expectedEntries);
+ assertThat(Arrays.asList(actualHistory.getPooledStringsToWrite()))
+ .containsExactlyElementsIn(expectedStrings);
+ }
+
+ @Test
+ public void testReadNotificationsWithNumberFilter_preExistingNotifs() throws Exception {
+ List<HistoricalNotification> expectedEntries = new ArrayList<>();
+ Set<String> expectedStrings = new HashSet<>();
+ int maxCount = 3;
+
+ NotificationHistory history = new NotificationHistory();
+ HistoricalNotification old1 = getHistoricalNotification(40);
+ history.addNotificationToWrite(old1);
+ expectedEntries.add(old1);
+
+ HistoricalNotification old2 = getHistoricalNotification(50);
+ history.addNotificationToWrite(old2);
+ expectedEntries.add(old2);
+ history.poolStringsFromNotifications();
+ expectedStrings.addAll(Arrays.asList(history.getPooledStringsToWrite()));
+
+ for (int i = 1; i < 10; i++) {
+ HistoricalNotification n = getHistoricalNotification(i);
+
+ if (i <= (maxCount - 2)) {
+ expectedStrings.add(n.getPackage());
+ expectedStrings.add(n.getChannelName());
+ expectedStrings.add(n.getChannelId());
+ expectedEntries.add(n);
+ }
+ history.addNotificationToWrite(n);
+ }
+ history.poolStringsFromNotifications();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ NotificationHistoryProtoHelper.write(baos, history, 1);
+
+ NotificationHistory actualHistory = new NotificationHistory();
+
+ NotificationHistoryFilter filter = new NotificationHistoryFilter.Builder()
+ .setMaxNotifications(maxCount)
+ .build();
+ NotificationHistoryProtoHelper.read(
+ new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
+ actualHistory,
+ filter);
+
+ assertThat(actualHistory.getNotificationsToWrite())
+ .containsExactlyElementsIn(expectedEntries);
+ assertThat(Arrays.asList(actualHistory.getPooledStringsToWrite()))
+ .containsExactlyElementsIn(expectedStrings);
+ }
+
+ @Test
+ public void testReadMergeIntoExistingHistory() throws Exception {
+ NotificationHistory history = new NotificationHistory();
+
+ List<HistoricalNotification> expectedEntries = new ArrayList<>();
+ Set<String> expectedStrings = new HashSet<>();
+ for (int i = 1; i < 10; i++) {
+ HistoricalNotification n = getHistoricalNotification(i);
+ expectedEntries.add(n);
+ expectedStrings.add(n.getPackage());
+ expectedStrings.add(n.getChannelName());
+ expectedStrings.add(n.getChannelId());
+ history.addNotificationToWrite(n);
+ }
+ history.poolStringsFromNotifications();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ NotificationHistoryProtoHelper.write(baos, history, 1);
+
+ // set up pre-existing notification history, as though read from a different file
+ NotificationHistory actualHistory = new NotificationHistory();
+ for (int i = 10; i < 20; i++) {
+ HistoricalNotification n = getHistoricalNotification(i);
+ expectedEntries.add(n);
+ expectedStrings.add(n.getPackage());
+ expectedStrings.add(n.getChannelName());
+ expectedStrings.add(n.getChannelId());
+ actualHistory.addNotificationToWrite(n);
+ }
+ actualHistory.poolStringsFromNotifications();
+
+ NotificationHistoryProtoHelper.read(
+ new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())),
+ actualHistory,
+ new NotificationHistoryFilter.Builder().build());
+
+ // Make sure history contains the original and new entries
+ assertThat(actualHistory.getNotificationsToWrite())
+ .containsExactlyElementsIn(expectedEntries);
+ assertThat(Arrays.asList(actualHistory.getPooledStringsToWrite()))
+ .containsExactlyElementsIn(expectedStrings);
+ }
+}
diff --git a/services/usage/java/com/android/server/usage/TEST_MAPPING b/services/usage/java/com/android/server/usage/TEST_MAPPING
new file mode 100644
index 000000000000..7b53d09fdbef
--- /dev/null
+++ b/services/usage/java/com/android/server/usage/TEST_MAPPING
@@ -0,0 +1,33 @@
+{
+ "presubmit": [
+ {
+ "name": "FrameworksCoreTests",
+ "options": [
+ {
+ "include-filter": "android.app.usage"
+ }
+ ]
+ },
+ {
+ "name": "FrameworksServicesTests",
+ "options": [
+ {
+ "include-filter": "com.android.server.usage"
+ },
+ {
+ "exclude-filter": "com.android.server.usage.StorageStatsServiceTest"
+ }
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "CtsUsageStatsTestCases",
+ "options": [
+ {
+ "include-filter": "android.app.usage.cts.UsageStatsTest"
+ }
+ ]
+ }
+ ]
+}
diff --git a/wifi/java/android/net/wifi/WifiConfiguration.java b/wifi/java/android/net/wifi/WifiConfiguration.java
index 3bedddc8ec7d..3dc5a628fe7c 100644
--- a/wifi/java/android/net/wifi/WifiConfiguration.java
+++ b/wifi/java/android/net/wifi/WifiConfiguration.java
@@ -1046,10 +1046,10 @@ public class WifiConfiguration implements Parcelable {
/**
* @hide
- * The wall clock time of when |mRandomizedMacAddress| last changed.
- * Used to determine when we should re-randomize in aggressive mode.
+ * The wall clock time of when |mRandomizedMacAddress| should be re-randomized in aggressive
+ * randomization mode.
*/
- public long randomizedMacLastModifiedTimeMs = 0;
+ public long randomizedMacExpirationTimeMs = 0;
/**
* @hide
@@ -1910,8 +1910,9 @@ public class WifiConfiguration implements Parcelable {
}
sbuf.append(" macRandomizationSetting: ").append(macRandomizationSetting).append("\n");
sbuf.append(" mRandomizedMacAddress: ").append(mRandomizedMacAddress).append("\n");
- sbuf.append(" randomizedMacLastModifiedTimeMs: ").append(randomizedMacLastModifiedTimeMs)
- .append("\n");
+ sbuf.append(" randomizedMacExpirationTimeMs: ")
+ .append(randomizedMacExpirationTimeMs == 0 ? "<none>"
+ : TimeUtils.logTimeOfDay(randomizedMacExpirationTimeMs)).append("\n");
sbuf.append(" KeyMgmt:");
for (int k = 0; k < this.allowedKeyManagement.size(); k++) {
if (this.allowedKeyManagement.get(k)) {
@@ -2439,7 +2440,7 @@ public class WifiConfiguration implements Parcelable {
recentFailure.setAssociationStatus(source.recentFailure.getAssociationStatus());
mRandomizedMacAddress = source.mRandomizedMacAddress;
macRandomizationSetting = source.macRandomizationSetting;
- randomizedMacLastModifiedTimeMs = source.randomizedMacLastModifiedTimeMs;
+ randomizedMacExpirationTimeMs = source.randomizedMacExpirationTimeMs;
requirePMF = source.requirePMF;
updateIdentifier = source.updateIdentifier;
}
@@ -2515,7 +2516,7 @@ public class WifiConfiguration implements Parcelable {
dest.writeParcelable(mRandomizedMacAddress, flags);
dest.writeInt(macRandomizationSetting);
dest.writeInt(osu ? 1 : 0);
- dest.writeLong(randomizedMacLastModifiedTimeMs);
+ dest.writeLong(randomizedMacExpirationTimeMs);
}
/** Implement the Parcelable interface {@hide} */
@@ -2591,7 +2592,7 @@ public class WifiConfiguration implements Parcelable {
config.mRandomizedMacAddress = in.readParcelable(null);
config.macRandomizationSetting = in.readInt();
config.osu = in.readInt() != 0;
- config.randomizedMacLastModifiedTimeMs = in.readLong();
+ config.randomizedMacExpirationTimeMs = in.readLong();
return config;
}