diff options
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; } |