diff options
35 files changed, 1469 insertions, 316 deletions
diff --git a/api/current.txt b/api/current.txt index f105bb8be094..de5a0b113a62 100644 --- a/api/current.txt +++ b/api/current.txt @@ -23397,6 +23397,7 @@ package android.media { method public int getStreamType(); method public boolean getTimestamp(android.media.AudioTimestamp); method public int getUnderrunCount(); + method public static boolean isDirectPlaybackSupported(android.media.AudioFormat, android.media.AudioAttributes); method public void pause() throws java.lang.IllegalStateException; method public void play() throws java.lang.IllegalStateException; method public void registerStreamEventCallback(java.util.concurrent.Executor, android.media.AudioTrack.StreamEventCallback); diff --git a/config/hiddenapi-greylist.txt b/config/hiddenapi-greylist.txt index 56dc4c1c0710..bacb991012fd 100644 --- a/config/hiddenapi-greylist.txt +++ b/config/hiddenapi-greylist.txt @@ -1639,10 +1639,6 @@ Landroid/widget/DigitalClock$FormatChangeObserver;-><init>(Landroid/widget/Digit Landroid/widget/QuickContactBadge$QueryHandler;-><init>(Landroid/widget/QuickContactBadge;Landroid/content/ContentResolver;)V Landroid/widget/RelativeLayout$DependencyGraph$Node;-><init>()V Landroid/widget/ScrollBarDrawable;-><init>()V -Lcom/android/i18n/phonenumbers/Phonenumber$PhoneNumber$CountryCodeSource;->values()[Lcom/android/i18n/phonenumbers/Phonenumber$PhoneNumber$CountryCodeSource; -Lcom/android/i18n/phonenumbers/PhoneNumberUtil$MatchType;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$MatchType; -Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberFormat;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberFormat; -Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberType;->values()[Lcom/android/i18n/phonenumbers/PhoneNumberUtil$PhoneNumberType; Lcom/android/ims/ImsCall;->deflect(Ljava/lang/String;)V Lcom/android/ims/ImsCall;->isMultiparty()Z Lcom/android/ims/ImsCall;->reject(I)V diff --git a/core/java/android/annotation/UnsupportedAppUsage.java b/core/java/android/annotation/UnsupportedAppUsage.java index 65e3f25f8fb6..ac3daaf638ad 100644 --- a/core/java/android/annotation/UnsupportedAppUsage.java +++ b/core/java/android/annotation/UnsupportedAppUsage.java @@ -26,16 +26,32 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; /** - * Indicates that a class member, that is not part of the SDK, is used by apps. - * Since the member is not part of the SDK, such use is not supported. + * Indicates that this non-SDK interface is used by apps. A non-SDK interface is a + * class member (field or method) that is not part of the public SDK. Since the + * member is not part of the SDK, usage by apps is not supported. * - * <p>This annotation acts as a heads up that changing a given method or field + * <h2>If you are an Android App developer</h2> + * + * This annotation indicates that you may be able to access the member, but that + * this access is discouraged and not supported by Android. If there is a value + * for {@link #maxTargetSdk()} on the annotation, access will be restricted based + * on the {@code targetSdkVersion} value set in your manifest. + * + * <p>Fields and methods annotated with this are likely to be restricted, changed + * or removed in future Android releases. If you rely on these members for + * functionality that is not otherwise supported by Android, consider filing a + * <a href="http://g.co/dev/appcompat">feature request</a>. + * + * <h2>If you are an Android OS developer</h2> + * + * This annotation acts as a heads up that changing a given method or field * may affect apps, potentially breaking them when the next Android version is * released. In some cases, for members that are heavily used, this annotation * may imply restrictions on changes to the member. * * <p>This annotation also results in access to the member being permitted by the - * runtime, with a warning being generated in debug builds. + * runtime, with a warning being generated in debug builds. Which apps can access + * the member is determined by the value of {@link #maxTargetSdk()}. * * <p>For more details, see go/UnsupportedAppUsage. * diff --git a/core/java/com/android/internal/os/AppIdToPackageMap.java b/core/java/com/android/internal/os/AppIdToPackageMap.java new file mode 100644 index 000000000000..65aa989bbb38 --- /dev/null +++ b/core/java/com/android/internal/os/AppIdToPackageMap.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2018 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.os; + + +import android.app.AppGlobals; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.os.UserHandle; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Maps AppIds to their package names. */ +public final class AppIdToPackageMap { + private final Map<Integer, String> mAppIdToPackageMap; + + @VisibleForTesting + public AppIdToPackageMap(Map<Integer, String> appIdToPackageMap) { + mAppIdToPackageMap = appIdToPackageMap; + } + + /** Creates a new {@link AppIdToPackageMap} for currently installed packages. */ + public static AppIdToPackageMap getSnapshot() { + List<PackageInfo> packages; + try { + packages = AppGlobals.getPackageManager() + .getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE + | PackageManager.MATCH_DIRECT_BOOT_AWARE, + UserHandle.USER_SYSTEM).getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + final Map<Integer, String> map = new HashMap<>(); + for (PackageInfo pkg : packages) { + final int uid = pkg.applicationInfo.uid; + if (pkg.sharedUserId != null && map.containsKey(uid)) { + // Use sharedUserId string as package name if there are collisions + map.put(uid, "shared:" + pkg.sharedUserId); + } else { + map.put(uid, pkg.packageName); + } + } + return new AppIdToPackageMap(map); + } + + /** Maps the AppId to a package name. */ + public String mapAppId(int appId) { + String pkgName = mAppIdToPackageMap.get(appId); + return pkgName == null ? String.valueOf(appId) : pkgName; + } + + /** Maps the UID to a package name. */ + public String mapUid(int uid) { + final int appId = UserHandle.getAppId(uid); + final String pkgName = mAppIdToPackageMap.get(appId); + final String uidStr = UserHandle.formatUid(uid); + return pkgName == null ? uidStr : pkgName + '/' + uidStr; + } +} diff --git a/core/java/com/android/internal/os/BinderCallsStats.java b/core/java/com/android/internal/os/BinderCallsStats.java index ff34036ce7e9..afed31d04b3c 100644 --- a/core/java/com/android/internal/os/BinderCallsStats.java +++ b/core/java/com/android/internal/os/BinderCallsStats.java @@ -22,7 +22,6 @@ import android.os.Binder; import android.os.Process; import android.os.SystemClock; import android.os.ThreadLocalWorkSource; -import android.os.UserHandle; import android.text.format.DateFormat; import android.util.ArrayMap; import android.util.Pair; @@ -356,14 +355,13 @@ public class BinderCallsStats implements BinderInternal.Observer { } /** Writes the collected statistics to the supplied {@link PrintWriter}.*/ - public void dump(PrintWriter pw, Map<Integer, String> appIdToPkgNameMap, boolean verbose) { + public void dump(PrintWriter pw, AppIdToPackageMap packageMap, boolean verbose) { synchronized (mLock) { - dumpLocked(pw, appIdToPkgNameMap, verbose); + dumpLocked(pw, packageMap, verbose); } } - private void dumpLocked(PrintWriter pw, Map<Integer, String> appIdToPkgNameMap, - boolean verbose) { + private void dumpLocked(PrintWriter pw, AppIdToPackageMap packageMap, boolean verbose) { long totalCallsCount = 0; long totalRecordedCallsCount = 0; long totalCpuTime = 0; @@ -397,9 +395,9 @@ public class BinderCallsStats implements BinderInternal.Observer { for (ExportedCallStat e : exportedCallStats) { sb.setLength(0); sb.append(" ") - .append(uidToString(e.callingUid, appIdToPkgNameMap)) + .append(packageMap.mapUid(e.callingUid)) .append(',') - .append(uidToString(e.workSourceUid, appIdToPkgNameMap)) + .append(packageMap.mapUid(e.workSourceUid)) .append(',').append(e.className) .append('#').append(e.methodName) .append(',').append(e.screenInteractive) @@ -420,7 +418,7 @@ public class BinderCallsStats implements BinderInternal.Observer { final List<UidEntry> summaryEntries = verbose ? entries : getHighestValues(entries, value -> value.cpuTimeMicros, 0.9); for (UidEntry entry : summaryEntries) { - String uidStr = uidToString(entry.workSourceUid, appIdToPkgNameMap); + String uidStr = packageMap.mapUid(entry.workSourceUid); pw.println(String.format(" %10d %3.0f%% %8d %8d %s", entry.cpuTimeMicros, 100d * entry.cpuTimeMicros / totalCpuTime, entry.recordedCallCount, entry.callCount, uidStr)); @@ -448,13 +446,6 @@ public class BinderCallsStats implements BinderInternal.Observer { } } - private static String uidToString(int uid, Map<Integer, String> pkgNameMap) { - final int appId = UserHandle.getAppId(uid); - final String pkgName = pkgNameMap == null ? null : pkgNameMap.get(appId); - final String uidStr = UserHandle.formatUid(uid); - return pkgName == null ? uidStr : pkgName + '/' + uidStr; - } - protected long getThreadTimeMicro() { return SystemClock.currentThreadTimeMicro(); } diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp index 7ebb2b289422..516093e42691 100644 --- a/core/jni/android_media_AudioTrack.cpp +++ b/core/jni/android_media_AudioTrack.cpp @@ -479,6 +479,24 @@ native_init_failure: } // ---------------------------------------------------------------------------- +static jboolean +android_media_AudioTrack_is_direct_output_supported(JNIEnv *env, jobject thiz, + jint encoding, jint sampleRate, + jint channelMask, jint channelIndexMask, + jint contentType, jint usage, jint flags) { + audio_config_base_t config = {}; + audio_attributes_t attributes = {}; + config.format = static_cast<audio_format_t>(audioFormatToNative(encoding)); + config.sample_rate = static_cast<uint32_t>(sampleRate); + config.channel_mask = nativeChannelMaskFromJavaChannelMasks(channelMask, channelIndexMask); + attributes.content_type = static_cast<audio_content_type_t>(contentType); + attributes.usage = static_cast<audio_usage_t>(usage); + attributes.flags = static_cast<audio_flags_mask_t>(flags); + // ignore source and tags attributes as they don't affect querying whether output is supported + return AudioTrack::isDirectOutputSupported(config, attributes); +} + +// ---------------------------------------------------------------------------- static void android_media_AudioTrack_start(JNIEnv *env, jobject thiz) { @@ -1297,6 +1315,9 @@ static jint android_media_AudioTrack_get_port_id(JNIEnv *env, jobject thiz) { // ---------------------------------------------------------------------------- static const JNINativeMethod gMethods[] = { // name, signature, funcPtr + {"native_is_direct_output_supported", + "(IIIIIII)Z", + (void *)android_media_AudioTrack_is_direct_output_supported}, {"native_start", "()V", (void *)android_media_AudioTrack_start}, {"native_stop", "()V", (void *)android_media_AudioTrack_stop}, {"native_pause", "()V", (void *)android_media_AudioTrack_pause}, diff --git a/core/proto/android/stats/devicepolicy/device_policy_enums.proto b/core/proto/android/stats/devicepolicy/device_policy_enums.proto index 5726d9ab0466..c7a6b68fbc00 100644 --- a/core/proto/android/stats/devicepolicy/device_policy_enums.proto +++ b/core/proto/android/stats/devicepolicy/device_policy_enums.proto @@ -51,7 +51,7 @@ enum EventId { SET_ALWAYS_ON_VPN_PACKAGE = 26; SET_PERMITTED_INPUT_METHODS = 27; SET_PERMITTED_ACCESSIBILITY_SERVICES = 28; - SET_SCREEN_CAPTURE_DISABLE = 29; + SET_SCREEN_CAPTURE_DISABLED = 29; SET_CAMERA_DISABLED = 30; QUERY_SUMMARY_FOR_USER = 31; QUERY_SUMMARY = 32; @@ -64,7 +64,7 @@ enum EventId { SET_ORGANIZATION_COLOR = 39; SET_PROFILE_NAME = 40; SET_USER_ICON = 41; - SET_DEVICE_OWNER_LOCKSCREEN_INFO = 42; + SET_DEVICE_OWNER_LOCK_SCREEN_INFO = 42; SET_SHORT_SUPPORT_MESSAGE = 43; SET_LONG_SUPPORT_MESSAGE = 44; SET_CROSS_PROFILE_CONTACTS_SEARCH_DISABLED = 45; @@ -139,4 +139,6 @@ enum EventId { SET_GLOBAL_SETTING = 111; PM_IS_INSTALLER_DEVICE_OWNER_OR_AFFILIATED_PROFILE_OWNER = 112; PM_UNINSTALL = 113; + WIFI_SERVICE_ADD_NETWORK_SUGGESTIONS = 114; + WIFI_SERVICE_ADD_OR_UPDATE_NETWORK = 115; } diff --git a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java index e2618191235d..97942a87da7f 100644 --- a/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java +++ b/core/tests/coretests/src/com/android/internal/os/BinderCallsStatsTest.java @@ -484,7 +484,7 @@ public class BinderCallsStatsTest { bcs.callEnded(callSession, REQUEST_SIZE, REPLY_SIZE); PrintWriter pw = new PrintWriter(new StringWriter()); - bcs.dump(pw, new HashMap<>(), true); + bcs.dump(pw, new AppIdToPackageMap(new HashMap<>()), true); } @Test diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index e4d47952b8c9..5e77fdf0a121 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -995,6 +995,35 @@ public class AudioTrack extends PlayerBase } } + /** + * Returns whether direct playback of an audio format with the provided attributes is + * currently supported on the system. + * <p>Direct playback means that the audio stream is not resampled or downmixed + * by the framework. Checking for direct support can help the app select the representation + * of audio content that most closely matches the capabilities of the device and peripherials + * (e.g. A/V receiver) connected to it. Note that the provided stream can still be re-encoded + * or mixed with other streams, if needed. + * <p>Also note that this query only provides information about the support of an audio format. + * It does not indicate whether the resources necessary for the playback are available + * at that instant. + * @param format a non-null {@link AudioFormat} instance describing the format of + * the audio data. + * @param attributes a non-null {@link AudioAttributes} instance. + * @return true if the given audio format can be played directly. + */ + public static boolean isDirectPlaybackSupported(@NonNull AudioFormat format, + @NonNull AudioAttributes attributes) { + if (format == null) { + throw new IllegalArgumentException("Illegal null AudioFormat argument"); + } + if (attributes == null) { + throw new IllegalArgumentException("Illegal null AudioAttributes argument"); + } + return native_is_direct_output_supported(format.getEncoding(), format.getSampleRate(), + format.getChannelMask(), format.getChannelIndexMask(), + attributes.getContentType(), attributes.getUsage(), attributes.getFlags()); + } + // mask of all the positional channels supported, however the allowed combinations // are further restricted by the matching left/right rule and // AudioSystem.OUT_CHANNEL_COUNT_MAX @@ -3328,6 +3357,9 @@ public class AudioTrack extends PlayerBase // Native methods called from the Java side //-------------------- + private static native boolean native_is_direct_output_supported(int encoding, int sampleRate, + int channelMask, int channelIndexMask, int contentType, int usage, int flags); + // post-condition: mStreamType is overwritten with a value // that reflects the audio attributes (e.g. an AudioAttributes object with a usage of // AudioAttributes.USAGE_MEDIA will map to AudioManager.STREAM_MUSIC diff --git a/packages/ExtServices/src/android/ext/services/notification/Assistant.java b/packages/ExtServices/src/android/ext/services/notification/Assistant.java index 0cad5af00267..133d8ba8357b 100644 --- a/packages/ExtServices/src/android/ext/services/notification/Assistant.java +++ b/packages/ExtServices/src/android/ext/services/notification/Assistant.java @@ -27,20 +27,15 @@ import android.app.ActivityThread; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; -import android.content.ContentResolver; import android.content.Context; import android.content.pm.IPackageManager; -import android.database.ContentObserver; import android.ext.services.notification.AgingHelper.Callback; -import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Environment; -import android.os.Handler; import android.os.SystemProperties; import android.os.UserHandle; import android.os.storage.StorageManager; -import android.provider.Settings; import android.service.notification.Adjustment; import android.service.notification.NotificationAssistantService; import android.service.notification.NotificationStats; @@ -92,8 +87,6 @@ public class Assistant extends NotificationAssistantService { PREJUDICAL_DISMISSALS.add(REASON_LISTENER_CANCEL); } - private float mDismissToViewRatioLimit; - private int mStreakLimit; private SmartActionsHelper mSmartActionsHelper; private NotificationCategorizer mNotificationCategorizer; private AgingHelper mAgingHelper; @@ -107,7 +100,11 @@ public class Assistant extends NotificationAssistantService { private Ranking mFakeRanking = null; private AtomicFile mFile = null; private IPackageManager mPackageManager; - protected SettingsObserver mSettingsObserver; + + @VisibleForTesting + protected AssistantSettings.Factory mSettingsFactory = AssistantSettings.FACTORY; + @VisibleForTesting + protected AssistantSettings mSettings; public Assistant() { } @@ -118,7 +115,8 @@ public class Assistant extends NotificationAssistantService { // Contexts are correctly hooked up by the creation step, which is required for the observer // to be hooked up/initialized. mPackageManager = ActivityThread.getPackageManager(); - mSettingsObserver = new SettingsObserver(mHandler); + mSettings = mSettingsFactory.createAndRegister(mHandler, + getApplicationContext().getContentResolver(), getUserId(), this::updateThresholds); mSmartActionsHelper = new SmartActionsHelper(); mNotificationCategorizer = new NotificationCategorizer(); mAgingHelper = new AgingHelper(getContext(), @@ -216,11 +214,11 @@ public class Assistant extends NotificationAssistantService { if (!isForCurrentUser(sbn)) { return null; } - NotificationEntry entry = new NotificationEntry( - ActivityThread.getPackageManager(), sbn, channel); + NotificationEntry entry = new NotificationEntry(mPackageManager, sbn, channel); ArrayList<Notification.Action> actions = - mSmartActionsHelper.suggestActions(this, entry); - ArrayList<CharSequence> replies = mSmartActionsHelper.suggestReplies(this, entry); + mSmartActionsHelper.suggestActions(this, entry, mSettings); + ArrayList<CharSequence> replies = + mSmartActionsHelper.suggestReplies(this, entry, mSettings); return createEnqueuedNotificationAdjustment(entry, actions, replies); } @@ -239,8 +237,7 @@ public class Assistant extends NotificationAssistantService { if (!smartReplies.isEmpty()) { signals.putCharSequenceArrayList(Adjustment.KEY_SMART_REPLIES, smartReplies); } - if (Settings.Secure.getInt(getContentResolver(), - Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, 1) == 1) { + if (mSettings.mNewInterruptionModel) { if (mNotificationCategorizer.shouldSilence(entry)) { final int importance = entry.getImportance() < IMPORTANCE_LOW ? entry.getImportance() : IMPORTANCE_LOW; @@ -460,6 +457,11 @@ public class Assistant extends NotificationAssistantService { } @VisibleForTesting + public void setSmartActionsHelper(SmartActionsHelper smartActionsHelper) { + mSmartActionsHelper = smartActionsHelper; + } + + @VisibleForTesting public ChannelImpressions getImpressions(String key) { synchronized (mkeyToImpressions) { return mkeyToImpressions.get(key); @@ -475,10 +477,20 @@ public class Assistant extends NotificationAssistantService { private ChannelImpressions createChannelImpressionsWithThresholds() { ChannelImpressions impressions = new ChannelImpressions(); - impressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit); + impressions.updateThresholds(mSettings.mDismissToViewRatioLimit, mSettings.mStreakLimit); return impressions; } + private void updateThresholds() { + // Update all existing channel impression objects with any new limits/thresholds. + synchronized (mkeyToImpressions) { + for (ChannelImpressions channelImpressions: mkeyToImpressions.values()) { + channelImpressions.updateThresholds( + mSettings.mDismissToViewRatioLimit, mSettings.mStreakLimit); + } + } + } + protected final class AgingCallback implements Callback { @Override public void sendAdjustment(String key, int newImportance) { @@ -495,51 +507,4 @@ public class Assistant extends NotificationAssistantService { } } - /** - * Observer for updates on blocking helper threshold values. - */ - protected final class SettingsObserver extends ContentObserver { - private final Uri STREAK_LIMIT_URI = - Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT); - private final Uri DISMISS_TO_VIEW_RATIO_LIMIT_URI = - Settings.Global.getUriFor( - Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT); - - public SettingsObserver(Handler handler) { - super(handler); - ContentResolver resolver = getApplicationContext().getContentResolver(); - resolver.registerContentObserver( - DISMISS_TO_VIEW_RATIO_LIMIT_URI, false, this, getUserId()); - resolver.registerContentObserver(STREAK_LIMIT_URI, false, this, getUserId()); - - // Update all uris on creation. - update(null); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - update(uri); - } - - private void update(Uri uri) { - ContentResolver resolver = getApplicationContext().getContentResolver(); - if (uri == null || DISMISS_TO_VIEW_RATIO_LIMIT_URI.equals(uri)) { - mDismissToViewRatioLimit = Settings.Global.getFloat( - resolver, Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, - ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT); - } - if (uri == null || STREAK_LIMIT_URI.equals(uri)) { - mStreakLimit = Settings.Global.getInt( - resolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, - ChannelImpressions.DEFAULT_STREAK_LIMIT); - } - - // Update all existing channel impression objects with any new limits/thresholds. - synchronized (mkeyToImpressions) { - for (ChannelImpressions channelImpressions: mkeyToImpressions.values()) { - channelImpressions.updateThresholds(mDismissToViewRatioLimit, mStreakLimit); - } - } - } - } } diff --git a/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java b/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java new file mode 100644 index 000000000000..39a1676b4ec7 --- /dev/null +++ b/packages/ExtServices/src/android/ext/services/notification/AssistantSettings.java @@ -0,0 +1,140 @@ +/** + * Copyright (C) 2018 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.ext.services.notification; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.util.KeyValueListParser; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Observes the settings for {@link Assistant}. + */ +final class AssistantSettings extends ContentObserver { + public static Factory FACTORY = AssistantSettings::createAndRegister; + private static final boolean DEFAULT_GENERATE_REPLIES = true; + private static final boolean DEFAULT_GENERATE_ACTIONS = true; + private static final int DEFAULT_NEW_INTERRUPTION_MODEL_INT = 1; + + private static final Uri STREAK_LIMIT_URI = + Settings.Global.getUriFor(Settings.Global.BLOCKING_HELPER_STREAK_LIMIT); + private static final Uri DISMISS_TO_VIEW_RATIO_LIMIT_URI = + Settings.Global.getUriFor( + Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT); + private static final Uri SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI = + Settings.Global.getUriFor( + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS); + private static final Uri NOTIFICATION_NEW_INTERRUPTION_MODEL_URI = + Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL); + + private static final String KEY_GENERATE_REPLIES = "generate_replies"; + private static final String KEY_GENERATE_ACTIONS = "generate_actions"; + + private final KeyValueListParser mParser = new KeyValueListParser(','); + private final ContentResolver mResolver; + private final int mUserId; + + @VisibleForTesting + protected final Runnable mOnUpdateRunnable; + + // Actuall configuration settings. + float mDismissToViewRatioLimit; + int mStreakLimit; + boolean mGenerateReplies = DEFAULT_GENERATE_REPLIES; + boolean mGenerateActions = DEFAULT_GENERATE_ACTIONS; + boolean mNewInterruptionModel; + + private AssistantSettings(Handler handler, ContentResolver resolver, int userId, + Runnable onUpdateRunnable) { + super(handler); + mResolver = resolver; + mUserId = userId; + mOnUpdateRunnable = onUpdateRunnable; + } + + private static AssistantSettings createAndRegister( + Handler handler, ContentResolver resolver, int userId, Runnable onUpdateRunnable) { + AssistantSettings assistantSettings = + new AssistantSettings(handler, resolver, userId, onUpdateRunnable); + assistantSettings.register(); + return assistantSettings; + } + + /** + * Creates an instance but doesn't register it as an observer. + */ + @VisibleForTesting + protected static AssistantSettings createForTesting( + Handler handler, ContentResolver resolver, int userId, Runnable onUpdateRunnable) { + return new AssistantSettings(handler, resolver, userId, onUpdateRunnable); + } + + private void register() { + mResolver.registerContentObserver( + DISMISS_TO_VIEW_RATIO_LIMIT_URI, false, this, mUserId); + mResolver.registerContentObserver(STREAK_LIMIT_URI, false, this, mUserId); + mResolver.registerContentObserver( + SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI, false, this, mUserId); + + // Update all uris on creation. + update(null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + update(uri); + } + + private void update(Uri uri) { + if (uri == null || DISMISS_TO_VIEW_RATIO_LIMIT_URI.equals(uri)) { + mDismissToViewRatioLimit = Settings.Global.getFloat( + mResolver, Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, + ChannelImpressions.DEFAULT_DISMISS_TO_VIEW_RATIO_LIMIT); + } + if (uri == null || STREAK_LIMIT_URI.equals(uri)) { + mStreakLimit = Settings.Global.getInt( + mResolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, + ChannelImpressions.DEFAULT_STREAK_LIMIT); + } + if (uri == null || SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS_URI.equals(uri)) { + mParser.setString( + Settings.Global.getString(mResolver, + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS)); + mGenerateReplies = + mParser.getBoolean(KEY_GENERATE_REPLIES, DEFAULT_GENERATE_REPLIES); + mGenerateActions = + mParser.getBoolean(KEY_GENERATE_ACTIONS, DEFAULT_GENERATE_ACTIONS); + } + if (uri == null || NOTIFICATION_NEW_INTERRUPTION_MODEL_URI.equals(uri)) { + int mNewInterruptionModelInt = Settings.Secure.getInt( + mResolver, Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, + DEFAULT_NEW_INTERRUPTION_MODEL_INT); + mNewInterruptionModel = mNewInterruptionModelInt == 1; + } + + mOnUpdateRunnable.run(); + } + + public interface Factory { + AssistantSettings createAndRegister(Handler handler, ContentResolver resolver, int userId, + Runnable onUpdateRunnable); + } +} diff --git a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java index 892267b22058..6f2b6c9dafd4 100644 --- a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java +++ b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java @@ -69,8 +69,11 @@ public class SmartActionsHelper { * from notification text / message, we can replace most of the code here by consuming that API. */ @NonNull - ArrayList<Notification.Action> suggestActions( - @Nullable Context context, @NonNull NotificationEntry entry) { + ArrayList<Notification.Action> suggestActions(@Nullable Context context, + @NonNull NotificationEntry entry, @NonNull AssistantSettings settings) { + if (!settings.mGenerateActions) { + return EMPTY_ACTION_LIST; + } if (!isEligibleForActionAdjustment(entry)) { return EMPTY_ACTION_LIST; } @@ -86,8 +89,11 @@ public class SmartActionsHelper { getMostSalientActionText(entry.getNotification()), MAX_SMART_ACTIONS); } - ArrayList<CharSequence> suggestReplies( - @Nullable Context context, @NonNull NotificationEntry entry) { + ArrayList<CharSequence> suggestReplies(@Nullable Context context, + @NonNull NotificationEntry entry, @NonNull AssistantSettings settings) { + if (!settings.mGenerateReplies) { + return EMPTY_REPLY_LIST; + } if (!isEligibleForReplyAdjustment(entry)) { return EMPTY_REPLY_LIST; } diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java new file mode 100644 index 000000000000..fd23f2b78b42 --- /dev/null +++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantSettingsTest.java @@ -0,0 +1,162 @@ +/** + * Copyright (C) 2018 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.ext.services.notification; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.ContentResolver; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.testing.TestableContext; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class AssistantSettingsTest { + private static final int USER_ID = 5; + + @Rule + public final TestableContext mContext = + new TestableContext(InstrumentationRegistry.getContext(), null); + + @Mock Runnable mOnUpdateRunnable; + + private ContentResolver mResolver; + private AssistantSettings mAssistantSettings; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mResolver = mContext.getContentResolver(); + Handler handler = new Handler(Looper.getMainLooper()); + + // To bypass real calls to global settings values, set the Settings values here. + Settings.Global.putFloat(mResolver, + Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, 0.8f); + Settings.Global.putInt(mResolver, Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, 2); + Settings.Global.putString(mResolver, + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, + "generate_replies=true,generate_actions=true"); + Settings.Secure.putInt(mResolver, Settings.Secure.NOTIFICATION_NEW_INTERRUPTION_MODEL, 1); + + mAssistantSettings = AssistantSettings.createForTesting( + handler, mResolver, USER_ID, mOnUpdateRunnable); + } + + @Test + public void testGenerateRepliesDisabled() { + Settings.Global.putString(mResolver, + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, + "generate_replies=false"); + + // Notify for the settings values we updated. + mAssistantSettings.onChange(false, + Settings.Global.getUriFor( + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS)); + + + assertFalse(mAssistantSettings.mGenerateReplies); + } + + @Test + public void testGenerateRepliesEnabled() { + Settings.Global.putString(mResolver, + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_replies=true"); + + // Notify for the settings values we updated. + mAssistantSettings.onChange(false, + Settings.Global.getUriFor( + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS)); + + assertTrue(mAssistantSettings.mGenerateReplies); + } + + @Test + public void testGenerateActionsDisabled() { + Settings.Global.putString(mResolver, + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_actions=false"); + + // Notify for the settings values we updated. + mAssistantSettings.onChange(false, + Settings.Global.getUriFor( + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS)); + + assertTrue(mAssistantSettings.mGenerateReplies); + } + + @Test + public void testGenerateActionsEnabled() { + Settings.Global.putString(mResolver, + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS, "generate_actions=true"); + + // Notify for the settings values we updated. + mAssistantSettings.onChange(false, + Settings.Global.getUriFor( + Settings.Global.SMART_SUGGESTIONS_IN_NOTIFICATIONS_FLAGS)); + + assertTrue(mAssistantSettings.mGenerateReplies); + } + + @Test + public void testStreakLimit() { + verify(mOnUpdateRunnable, never()).run(); + + // Update settings value. + int newStreakLimit = 4; + Settings.Global.putInt(mResolver, + Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, newStreakLimit); + + // Notify for the settings value we updated. + mAssistantSettings.onChange(false, Settings.Global.getUriFor( + Settings.Global.BLOCKING_HELPER_STREAK_LIMIT)); + + assertEquals(newStreakLimit, mAssistantSettings.mStreakLimit); + verify(mOnUpdateRunnable).run(); + } + + @Test + public void testDismissToViewRatioLimit() { + verify(mOnUpdateRunnable, never()).run(); + + // Update settings value. + float newDismissToViewRatioLimit = 3f; + Settings.Global.putFloat(mResolver, + Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, + newDismissToViewRatioLimit); + + // Notify for the settings value we updated. + mAssistantSettings.onChange(false, Settings.Global.getUriFor( + Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT)); + + assertEquals(newDismissToViewRatioLimit, mAssistantSettings.mDismissToViewRatioLimit, 1e-6); + verify(mOnUpdateRunnable).run(); + } +} diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java index 2eb005a9b1fa..0a95b83bdbe3 100644 --- a/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java +++ b/packages/ExtServices/tests/src/android/ext/services/notification/AssistantTest.java @@ -33,13 +33,11 @@ import android.app.Application; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; -import android.content.ContentResolver; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.os.Build; import android.os.UserHandle; -import android.provider.Settings; import android.service.notification.Adjustment; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.Ranking; @@ -86,8 +84,7 @@ public class AssistantTest extends ServiceTestCase<Assistant> { @Mock INotificationManager mNoMan; @Mock AtomicFile mFile; - @Mock - IPackageManager mPackageManager; + @Mock IPackageManager mPackageManager; Assistant mAssistant; Application mApplication; @@ -108,20 +105,26 @@ public class AssistantTest extends ServiceTestCase<Assistant> { new Intent("android.service.notification.NotificationAssistantService"); startIntent.setPackage("android.ext.services"); - // To bypass real calls to global settings values, set the Settings values here. - Settings.Global.putFloat(mContext.getContentResolver(), - Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, 0.8f); - Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, 2); mApplication = (Application) InstrumentationRegistry.getInstrumentation(). getTargetContext().getApplicationContext(); // Force the test to use the correct application instead of trying to use a mock application setApplication(mApplication); - bindService(startIntent); + + setupService(); mAssistant = getService(); + + // Override the AssistantSettings factory. + mAssistant.mSettingsFactory = AssistantSettings::createForTesting; + + bindService(startIntent); + + mAssistant.mSettings.mDismissToViewRatioLimit = 0.8f; + mAssistant.mSettings.mStreakLimit = 2; + mAssistant.mSettings.mNewInterruptionModel = true; mAssistant.setNoMan(mNoMan); mAssistant.setFile(mFile); mAssistant.setPackageManager(mPackageManager); + ApplicationInfo info = mock(ApplicationInfo.class); when(mPackageManager.getApplicationInfo(anyString(), anyInt(), anyInt())) .thenReturn(info); @@ -408,6 +411,8 @@ public class AssistantTest extends ServiceTestCase<Assistant> { mAssistant.writeXml(serializer); Assistant assistant = new Assistant(); + // onCreate is not invoked, so settings won't be initialised, unless we do it here. + assistant.mSettings = mAssistant.mSettings; assistant.readXml(new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray()))); assertEquals(ci1, assistant.getImpressions(key1)); @@ -417,8 +422,6 @@ public class AssistantTest extends ServiceTestCase<Assistant> { @Test public void testSettingsProviderUpdate() { - ContentResolver resolver = mApplication.getContentResolver(); - // Set up channels String key = mAssistant.getKey("pkg1", 1, "channel1"); ChannelImpressions ci = new ChannelImpressions(); @@ -435,19 +438,11 @@ public class AssistantTest extends ServiceTestCase<Assistant> { assertEquals(false, ci.shouldTriggerBlock()); // Update settings values. - float newDismissToViewRatioLimit = 0f; - int newStreakLimit = 0; - Settings.Global.putFloat(resolver, - Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT, - newDismissToViewRatioLimit); - Settings.Global.putInt(resolver, - Settings.Global.BLOCKING_HELPER_STREAK_LIMIT, newStreakLimit); + mAssistant.mSettings.mDismissToViewRatioLimit = 0f; + mAssistant.mSettings.mStreakLimit = 0; // Notify for the settings values we updated. - mAssistant.mSettingsObserver.onChange(false, Settings.Global.getUriFor( - Settings.Global.BLOCKING_HELPER_STREAK_LIMIT)); - mAssistant.mSettingsObserver.onChange(false, Settings.Global.getUriFor( - Settings.Global.BLOCKING_HELPER_DISMISS_TO_VIEW_RATIO_LIMIT)); + mAssistant.mSettings.mOnUpdateRunnable.run(); // With the new threshold, the blocking helper should be triggered. assertEquals(true, ci.shouldTriggerBlock()); diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index cc17b25d9a40..0126e7e59915 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -18,6 +18,7 @@ android_library { "SettingsLibSettingsSpinner", "SettingsLayoutPreference", "ActionButtonsPreference", + "SettingsLibEntityHeaderWidgets", ], // ANDROIDMK TRANSLATION ERROR: unsupported assignment to LOCAL_SHARED_JAVA_LIBRARIES diff --git a/packages/SettingsLib/EntityHeaderWidgets/Android.bp b/packages/SettingsLib/EntityHeaderWidgets/Android.bp new file mode 100644 index 000000000000..3ca4ecd33ce4 --- /dev/null +++ b/packages/SettingsLib/EntityHeaderWidgets/Android.bp @@ -0,0 +1,14 @@ +android_library { + name: "SettingsLibEntityHeaderWidgets", + + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + + static_libs: [ + "androidx.annotation_annotation", + "SettingsLibAppPreference" + ], + + sdk_version: "system_current", + min_sdk_version: "21", +} diff --git a/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml b/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml new file mode 100644 index 000000000000..4b9f1ab8d6cc --- /dev/null +++ b/packages/SettingsLib/EntityHeaderWidgets/AndroidManifest.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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" + package="com.android.settingslib.widget"> + + <uses-sdk android:minSdkVersion="21" /> + +</manifest> diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml new file mode 100644 index 000000000000..9f30eda242f6 --- /dev/null +++ b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_entities_header.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/app_entities_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="24dp" + android:paddingEnd="8dp" + android:gravity="center" + android:orientation="vertical"> + + <TextView + android:id="@+id/header_title" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:gravity="center" + android:textAppearance="@style/AppEntitiesHeader.Text.HeaderTitle"/> + + <LinearLayout + android:id="@+id/all_apps_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:gravity="center"> + + <include + android:id="@+id/app1_view" + layout="@layout/app_view"/> + + <include + android:id="@+id/app2_view" + layout="@layout/app_view"/> + + <include + android:id="@+id/app3_view" + layout="@layout/app_view"/> + + </LinearLayout> + + <Button + android:id="@+id/header_details" + style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:gravity="center"/> + +</LinearLayout> diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml new file mode 100644 index 000000000000..fcafa3140955 --- /dev/null +++ b/packages/SettingsLib/EntityHeaderWidgets/res/layout/app_view.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginEnd="16dp" + android:gravity="center" + android:orientation="vertical"> + + <ImageView + android:id="@+id/app_icon" + android:layout_width="@dimen/secondary_app_icon_size" + android:layout_height="@dimen/secondary_app_icon_size" + android:layout_marginBottom="12dp"/> + + <TextView + android:id="@+id/app_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="2dp" + android:singleLine="true" + android:ellipsize="marquee" + android:textAppearance="@style/AppEntitiesHeader.Text.Title"/> + + <TextView + android:id="@+id/app_summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:ellipsize="marquee" + android:textAppearance="@style/AppEntitiesHeader.Text.Summary"/> + +</LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml b/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml new file mode 100644 index 000000000000..0eefd4bff97f --- /dev/null +++ b/packages/SettingsLib/EntityHeaderWidgets/res/values/styles.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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> + <style name="AppEntitiesHeader.Text" + parent="@android:style/TextAppearance.Material.Subhead"> + <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + </style> + + <style name="AppEntitiesHeader.Text.HeaderTitle"> + <item name="android:textSize">14sp</item> + </style> + + <style name="AppEntitiesHeader.Text.Title"> + <item name="android:textSize">16sp</item> + </style> + + <style name="AppEntitiesHeader.Text.Summary" + parent="@android:style/TextAppearance.Material.Body1"> + <item name="android:textColor">?android:attr/textColorSecondary</item> + <item name="android:textSize">14sp</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java b/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java new file mode 100644 index 000000000000..8ccf89fc38b0 --- /dev/null +++ b/packages/SettingsLib/EntityHeaderWidgets/src/com/android/settingslib/widget/AppEntitiesHeaderController.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2018 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.settingslib.widget; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.VisibleForTesting; + +/** + * This is used to initialize view which was inflated + * from {@link R.xml.app_entities_header.xml}. + * + * <p>The view looks like below. + * + * <pre> + * -------------------------------------------------------------- + * | Header title | + * -------------------------------------------------------------- + * | App1 icon | App2 icon | App3 icon | + * | App1 title | App2 title | App3 title | + * | App1 summary | App2 summary | App3 summary | + * |------------------------------------------------------------- + * | Header details | + * -------------------------------------------------------------- + * </pre> + * + * <p>How to use AppEntitiesHeaderController? + * + * <p>1. Add a {@link LayoutPreference} in layout XML file. + * <pre> + * <com.android.settingslib.widget.LayoutPreference + * android:key="app_entities_header" + * android:layout="@layout/app_entities_header"/> + * </pre> + * + * <p>2. Use AppEntitiesHeaderController to call below methods, then you can initialize + * view of <code>app_entities_header</code>. + * + * <pre> + * + * View headerView = ((LayoutPreference) screen.findPreference("app_entities_header")) + * .findViewById(R.id.app_entities_header); + * + * AppEntitiesHeaderController.newInstance(context, headerView) + * .setHeaderTitleRes(R.string.xxxxx) + * .setHeaderDetailsRes(R.string.xxxxx) + * .setHeaderDetailsClickListener(onClickListener) + * .setAppEntity(0, icon, "app title", "app summary") + * .setAppEntity(1, icon, "app title", "app summary") + * .setAppEntity(2, icon, "app title", "app summary") + * .apply(); + * </pre> + */ +public class AppEntitiesHeaderController { + + private static final String TAG = "AppEntitiesHeaderCtl"; + + @VisibleForTesting + static final int MAXIMUM_APPS = 3; + + private final Context mContext; + private final TextView mHeaderTitleView; + private final Button mHeaderDetailsView; + + private final AppEntity[] mAppEntities; + private final View[] mAppEntityViews; + private final ImageView[] mAppIconViews; + private final TextView[] mAppTitleViews; + private final TextView[] mAppSummaryViews; + + private int mHeaderTitleRes; + private int mHeaderDetailsRes; + private View.OnClickListener mDetailsOnClickListener; + + /** + * Creates a new instance of the controller. + * + * @param context the Context the view is running in + * @param appEntitiesHeaderView view was inflated from <code>app_entities_header</code> + */ + public static AppEntitiesHeaderController newInstance(@NonNull Context context, + @NonNull View appEntitiesHeaderView) { + return new AppEntitiesHeaderController(context, appEntitiesHeaderView); + } + + private AppEntitiesHeaderController(Context context, View appEntitiesHeaderView) { + mContext = context; + mHeaderTitleView = appEntitiesHeaderView.findViewById(R.id.header_title); + mHeaderDetailsView = appEntitiesHeaderView.findViewById(R.id.header_details); + + mAppEntities = new AppEntity[MAXIMUM_APPS]; + mAppIconViews = new ImageView[MAXIMUM_APPS]; + mAppTitleViews = new TextView[MAXIMUM_APPS]; + mAppSummaryViews = new TextView[MAXIMUM_APPS]; + + mAppEntityViews = new View[]{ + appEntitiesHeaderView.findViewById(R.id.app1_view), + appEntitiesHeaderView.findViewById(R.id.app2_view), + appEntitiesHeaderView.findViewById(R.id.app3_view) + }; + + // Initialize view in advance, so we won't take too much time to do it when controller is + // binding view. + for (int index = 0; index < MAXIMUM_APPS; index++) { + final View appView = mAppEntityViews[index]; + mAppIconViews[index] = (ImageView) appView.findViewById(R.id.app_icon); + mAppTitleViews[index] = (TextView) appView.findViewById(R.id.app_title); + mAppSummaryViews[index] = (TextView) appView.findViewById(R.id.app_summary); + } + } + + /** + * Set the text resource for app entities header title. + */ + public AppEntitiesHeaderController setHeaderTitleRes(@StringRes int titleRes) { + mHeaderTitleRes = titleRes; + return this; + } + + /** + * Set the text resource for app entities header details. + */ + public AppEntitiesHeaderController setHeaderDetailsRes(@StringRes int detailsRes) { + mHeaderDetailsRes = detailsRes; + return this; + } + + /** + * Register a callback to be invoked when header details view is clicked. + */ + public AppEntitiesHeaderController setHeaderDetailsClickListener( + @Nullable View.OnClickListener clickListener) { + mDetailsOnClickListener = clickListener; + return this; + } + + /** + * Set an app entity at a specified position view. + * + * @param index the index at which the specified view is to be inserted + * @param icon the icon of app entity + * @param titleRes the title of app entity + * @param summaryRes the summary of app entity + * @return this {@code AppEntitiesHeaderController} object + */ + public AppEntitiesHeaderController setAppEntity(int index, @NonNull Drawable icon, + @Nullable CharSequence titleRes, @Nullable CharSequence summaryRes) { + final AppEntity appEntity = new AppEntity(icon, titleRes, summaryRes); + mAppEntities[index] = appEntity; + return this; + } + + /** + * Remove an app entity at a specified position view. + * + * @param index the index at which the specified view is to be removed + * @return this {@code AppEntitiesHeaderController} object + */ + public AppEntitiesHeaderController removeAppEntity(int index) { + mAppEntities[index] = null; + return this; + } + + /** + * Clear all app entities in app entities header. + * + * @return this {@code AppEntitiesHeaderController} object + */ + public AppEntitiesHeaderController clearAllAppEntities() { + for (int index = 0; index < MAXIMUM_APPS; index++) { + removeAppEntity(index); + } + return this; + } + + /** + * Done mutating app entities header, rebinds everything. + */ + public void apply() { + bindHeaderTitleView(); + bindHeaderDetailsView(); + + // Rebind all apps view + for (int index = 0; index < MAXIMUM_APPS; index++) { + bindAppEntityView(index); + } + } + + private void bindHeaderTitleView() { + CharSequence titleText = ""; + try { + titleText = mContext.getText(mHeaderTitleRes); + } catch (Resources.NotFoundException e) { + Log.e(TAG, "Resource of header title can't not be found!", e); + } + mHeaderTitleView.setText(titleText); + mHeaderTitleView.setVisibility( + TextUtils.isEmpty(titleText) ? View.GONE : View.VISIBLE); + } + + private void bindHeaderDetailsView() { + CharSequence detailsText = ""; + try { + detailsText = mContext.getText(mHeaderDetailsRes); + } catch (Resources.NotFoundException e) { + Log.e(TAG, "Resource of header details can't not be found!", e); + } + mHeaderDetailsView.setText(detailsText); + mHeaderDetailsView.setVisibility( + TextUtils.isEmpty(detailsText) ? View.GONE : View.VISIBLE); + mHeaderDetailsView.setOnClickListener(mDetailsOnClickListener); + } + + private void bindAppEntityView(int index) { + final AppEntity appEntity = mAppEntities[index]; + mAppEntityViews[index].setVisibility(appEntity != null ? View.VISIBLE : View.GONE); + + if (appEntity != null) { + mAppIconViews[index].setImageDrawable(appEntity.icon); + + mAppTitleViews[index].setVisibility( + TextUtils.isEmpty(appEntity.title) ? View.INVISIBLE : View.VISIBLE); + mAppTitleViews[index].setText(appEntity.title); + + mAppSummaryViews[index].setVisibility( + TextUtils.isEmpty(appEntity.summary) ? View.INVISIBLE : View.VISIBLE); + mAppSummaryViews[index].setText(appEntity.summary); + } + } + + private static class AppEntity { + public final Drawable icon; + public final CharSequence title; + public final CharSequence summary; + + AppEntity(Drawable appIcon, CharSequence appTitle, CharSequence appSummary) { + icon = appIcon; + title = appTitle; + summary = appSummary; + } + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java new file mode 100644 index 000000000000..c3bc8da89685 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/AppEntitiesHeaderControllerTest.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2018 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.settingslib.widget; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class AppEntitiesHeaderControllerTest { + + private static final CharSequence TITLE = "APP_TITLE"; + private static final CharSequence SUMMARY = "APP_SUMMARY"; + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Context mContext; + private Drawable mIcon; + private View mAppEntitiesHeaderView; + private AppEntitiesHeaderController mController; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mAppEntitiesHeaderView = LayoutInflater.from(mContext).inflate( + R.layout.app_entities_header, null /* root */); + mIcon = mContext.getDrawable(R.drawable.ic_menu); + mController = AppEntitiesHeaderController.newInstance(mContext, + mAppEntitiesHeaderView); + } + + @Test + public void assert_amountOfMaximumAppsAreThree() { + assertThat(AppEntitiesHeaderController.MAXIMUM_APPS).isEqualTo(3); + } + + @Test + public void setHeaderTitleRes_setTextRes_shouldSetToTitleView() { + mController.setHeaderTitleRes(R.string.expand_button_title).apply(); + final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_title); + + assertThat(view.getText()).isEqualTo(mContext.getText(R.string.expand_button_title)); + } + + @Test + public void setHeaderDetailsRes_setTextRes_shouldSetToDetailsView() { + mController.setHeaderDetailsRes(R.string.expand_button_title).apply(); + final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_details); + + assertThat(view.getText()).isEqualTo(mContext.getText(R.string.expand_button_title)); + } + + @Test + public void setHeaderDetailsClickListener_setClickListener_detailsViewAttachClickListener() { + mController.setHeaderDetailsClickListener(v -> { + }).apply(); + final TextView view = mAppEntitiesHeaderView.findViewById(R.id.header_details); + + assertThat(view.hasOnClickListeners()).isTrue(); + } + + @Test + public void setAppEntity_indexLessThanZero_shouldThrowArrayIndexOutOfBoundsException() { + thrown.expect(ArrayIndexOutOfBoundsException.class); + + mController.setAppEntity(-1, mIcon, TITLE, SUMMARY); + } + + @Test + public void asetAppEntity_indexGreaterThanMaximum_shouldThrowArrayIndexOutOfBoundsException() { + thrown.expect(ArrayIndexOutOfBoundsException.class); + + mController.setAppEntity(AppEntitiesHeaderController.MAXIMUM_APPS + 1, mIcon, TITLE, + SUMMARY); + } + + @Test + public void setAppEntity_addAppToIndex0_shouldShowAppView1() { + mController.setAppEntity(0, mIcon, TITLE, SUMMARY).apply(); + final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view); + final ImageView appIconView = app1View.findViewById(R.id.app_icon); + final TextView appTitle = app1View.findViewById(R.id.app_title); + final TextView appSummary = app1View.findViewById(R.id.app_summary); + + assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(appIconView.getDrawable()).isNotNull(); + assertThat(appTitle.getText()).isEqualTo(TITLE); + assertThat(appSummary.getText()).isEqualTo(SUMMARY); + } + + @Test + public void setAppEntity_addAppToIndex1_shouldShowAppView2() { + mController.setAppEntity(1, mIcon, TITLE, SUMMARY).apply(); + final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view); + final ImageView appIconView = app2View.findViewById(R.id.app_icon); + final TextView appTitle = app2View.findViewById(R.id.app_title); + final TextView appSummary = app2View.findViewById(R.id.app_summary); + + assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(appIconView.getDrawable()).isNotNull(); + assertThat(appTitle.getText()).isEqualTo(TITLE); + assertThat(appSummary.getText()).isEqualTo(SUMMARY); + } + + @Test + public void setAppEntity_addAppToIndex2_shouldShowAppView3() { + mController.setAppEntity(2, mIcon, TITLE, SUMMARY).apply(); + final View app3View = mAppEntitiesHeaderView.findViewById(R.id.app3_view); + final ImageView appIconView = app3View.findViewById(R.id.app_icon); + final TextView appTitle = app3View.findViewById(R.id.app_title); + final TextView appSummary = app3View.findViewById(R.id.app_summary); + + assertThat(app3View.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(appIconView.getDrawable()).isNotNull(); + assertThat(appTitle.getText()).isEqualTo(TITLE); + assertThat(appSummary.getText()).isEqualTo(SUMMARY); + } + + @Test + public void removeAppEntity_removeIndex0_shouldNotShowAppView1() { + mController.setAppEntity(0, mIcon, TITLE, SUMMARY) + .setAppEntity(1, mIcon, TITLE, SUMMARY).apply(); + final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view); + final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view); + + assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE); + + mController.removeAppEntity(0).apply(); + + assertThat(app1View.getVisibility()).isEqualTo(View.GONE); + assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void clearAllAppEntities_shouldNotShowAllAppViews() { + mController.setAppEntity(0, mIcon, TITLE, SUMMARY) + .setAppEntity(1, mIcon, TITLE, SUMMARY) + .setAppEntity(2, mIcon, TITLE, SUMMARY).apply(); + final View app1View = mAppEntitiesHeaderView.findViewById(R.id.app1_view); + final View app2View = mAppEntitiesHeaderView.findViewById(R.id.app2_view); + final View app3View = mAppEntitiesHeaderView.findViewById(R.id.app3_view); + + assertThat(app1View.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(app2View.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(app3View.getVisibility()).isEqualTo(View.VISIBLE); + + mController.clearAllAppEntities().apply(); + assertThat(app1View.getVisibility()).isEqualTo(View.GONE); + assertThat(app2View.getVisibility()).isEqualTo(View.GONE); + assertThat(app3View.getVisibility()).isEqualTo(View.GONE); + } +} diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index b6c9b8c8cee8..cb860ae0b8b4 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -361,6 +361,7 @@ <!-- The height of the qs customize header. Should be (28dp - qs_tile_margin_top_bottom). --> <dimen name="qs_customize_header_min_height">40dp</dimen> <dimen name="qs_tile_margin_top">18dp</dimen> + <dimen name="qs_tile_background_size">40dp</dimen> <dimen name="qs_quick_tile_size">48dp</dimen> <!-- Maximum width of quick quick settings panel. Defaults to MATCH_PARENT--> <dimen name="qs_quick_layout_width">-1px</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java index 3a96595dee06..32fd2dcedd0e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileBaseView.java @@ -19,14 +19,19 @@ import android.animation.ValueAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; +import android.graphics.Path; +import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.PathShape; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.service.quicksettings.Tile; import android.text.TextUtils; import android.util.Log; +import android.util.PathParser; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -46,6 +51,7 @@ import com.android.systemui.plugins.qs.QSTile.BooleanState; public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView { private static final String TAG = "QSTileBaseView"; + private static final int ICON_MASK_ID = com.android.internal.R.string.config_icon_mask; private final H mHandler = new H(); private final int[] mLocInScreen = new int[2]; private final FrameLayout mIconFrame; @@ -62,6 +68,7 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView { private final int mColorInactive; private final int mColorDisabled; private int mCircleColor; + private int mBgSize; public QSTileBaseView(Context context, QSIconView icon) { this(context, icon, false); @@ -71,15 +78,23 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView { super(context); // Default to Quick Tile padding, and QSTileView will specify its own padding. int padding = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding); - mIconFrame = new FrameLayout(context); mIconFrame.setForegroundGravity(Gravity.CENTER); int size = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size); addView(mIconFrame, new LayoutParams(size, size)); mBg = new ImageView(getContext()); + Path path = new Path(PathParser.createPathFromPathData( + context.getResources().getString(ICON_MASK_ID))); + float pathSize = AdaptiveIconDrawable.MASK_SIZE; + PathShape p = new PathShape(path, pathSize, pathSize); + ShapeDrawable d = new ShapeDrawable(p); + int bgSize = context.getResources().getDimensionPixelSize(R.dimen.qs_tile_background_size); + d.setIntrinsicHeight(bgSize); + d.setIntrinsicWidth(bgSize); mBg.setScaleType(ScaleType.FIT_CENTER); - mBg.setImageResource(R.drawable.ic_qs_circle); - mIconFrame.addView(mBg); + mBg.setImageDrawable(d); + mIconFrame.addView(mBg, ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); mIcon = icon; FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); @@ -107,7 +122,7 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView { setFocusable(true); } - public View getBgCicle() { + public View getBgCircle() { return mBg; } @@ -303,6 +318,7 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView { private class H extends Handler { private static final int STATE_CHANGED = 1; + public H() { super(Looper.getMainLooper()); } @@ -314,4 +330,4 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView { } } } -} +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java index 11a2fc9c1e45..13925baa7010 100644 --- a/services/core/java/com/android/server/BinderCallsStatsService.java +++ b/services/core/java/com/android/server/BinderCallsStatsService.java @@ -19,7 +19,6 @@ package com.android.server; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; -import android.app.AppGlobals; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -28,7 +27,6 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Binder; import android.os.Process; -import android.os.RemoteException; import android.os.SystemProperties; import android.os.ThreadLocalWorkSource; import android.os.UserHandle; @@ -39,6 +37,7 @@ import android.util.KeyValueListParser; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.AppIdToPackageMap; import com.android.internal.os.BackgroundThread; import com.android.internal.os.BinderCallsStats; import com.android.internal.os.BinderInternal; @@ -48,9 +47,7 @@ import com.android.internal.os.CachedDeviceState; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; public class BinderCallsStatsService extends Binder { @@ -82,11 +79,11 @@ public class BinderCallsStatsService extends Binder { mAppIdWhitelist = createAppidWhitelist(context); } - public void dump(PrintWriter pw, Map<Integer, String> appIdToPackageName) { + public void dump(PrintWriter pw, AppIdToPackageMap packageMap) { pw.println("AppIds of apps that can set the work source:"); final ArraySet<Integer> whitelist = mAppIdWhitelist; for (Integer appId : whitelist) { - pw.println("\t- " + appIdToPackageName.getOrDefault(appId, String.valueOf(appId))); + pw.println("\t- " + packageMap.mapAppId(appId)); } } @@ -361,7 +358,7 @@ public class BinderCallsStatsService extends Binder { pw.println("Detailed tracking disabled"); return; } else if ("--dump-worksource-provider".equals(arg)) { - mWorkSourceProvider.dump(pw, getAppIdToPackagesMap()); + mWorkSourceProvider.dump(pw, AppIdToPackageMap.getSnapshot()); return; } else if ("-h".equals(arg)) { pw.println("binder_calls_stats commands:"); @@ -377,28 +374,6 @@ public class BinderCallsStatsService extends Binder { } } } - mBinderCallsStats.dump(pw, getAppIdToPackagesMap(), verbose); - } - - private Map<Integer, String> getAppIdToPackagesMap() { - List<PackageInfo> packages; - try { - packages = AppGlobals.getPackageManager() - .getInstalledPackages(PackageManager.MATCH_UNINSTALLED_PACKAGES, - UserHandle.USER_SYSTEM).getList(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - Map<Integer,String> map = new HashMap<>(); - for (PackageInfo pkg : packages) { - String name = pkg.packageName; - int uid = pkg.applicationInfo.uid; - // Use sharedUserId string as package name if there are collisions - if (pkg.sharedUserId != null && map.containsKey(uid)) { - name = "shared:" + pkg.sharedUserId; - } - map.put(uid, name); - } - return map; + mBinderCallsStats.dump(pw, AppIdToPackageMap.getSnapshot(), verbose); } } diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 14503f9d7379..eda9fe15fe36 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -902,6 +902,7 @@ public class ConnectivityService extends IConnectivityManager.Stub // Listen to package add and removal events for all users. intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); intentFilter.addDataScheme("package"); mContext.registerReceiverAsUser( @@ -4203,12 +4204,46 @@ public class ConnectivityService extends IConnectivityManager.Stub mPermissionMonitor.onPackageAdded(packageName, uid); } - private void onPackageRemoved(String packageName, int uid) { + private void onPackageReplaced(String packageName, int uid) { + if (TextUtils.isEmpty(packageName) || uid < 0) { + Slog.wtf(TAG, "Invalid package in onPackageReplaced: " + packageName + " | " + uid); + return; + } + final int userId = UserHandle.getUserId(uid); + synchronized (mVpns) { + final Vpn vpn = mVpns.get(userId); + if (vpn == null) { + return; + } + // Legacy always-on VPN won't be affected since the package name is not set. + if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName)) { + Slog.d(TAG, "Restarting always-on VPN package " + packageName + " for user " + + userId); + vpn.startAlwaysOnVpn(); + } + } + } + + private void onPackageRemoved(String packageName, int uid, boolean isReplacing) { if (TextUtils.isEmpty(packageName) || uid < 0) { Slog.wtf(TAG, "Invalid package in onPackageRemoved: " + packageName + " | " + uid); return; } mPermissionMonitor.onPackageRemoved(uid); + + final int userId = UserHandle.getUserId(uid); + synchronized (mVpns) { + final Vpn vpn = mVpns.get(userId); + if (vpn == null) { + return; + } + // Legacy always-on VPN won't be affected since the package name is not set. + if (TextUtils.equals(vpn.getAlwaysOnPackage(), packageName) && !isReplacing) { + Slog.d(TAG, "Removing always-on VPN package " + packageName + " for user " + + userId); + vpn.setAlwaysOnPackage(null, false); + } + } } private void onUserUnlocked(int userId) { @@ -4245,8 +4280,12 @@ public class ConnectivityService extends IConnectivityManager.Stub onUserUnlocked(userId); } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { onPackageAdded(packageName, uid); + } else if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) { + onPackageReplaced(packageName, uid); } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { - onPackageRemoved(packageName, uid); + final boolean isReplacing = intent.getBooleanExtra( + Intent.EXTRA_REPLACING, false); + onPackageRemoved(packageName, uid, isReplacing); } } }; diff --git a/services/core/java/com/android/server/LooperStatsService.java b/services/core/java/com/android/server/LooperStatsService.java index fa3babad639d..cee98c10c7f7 100644 --- a/services/core/java/com/android/server/LooperStatsService.java +++ b/services/core/java/com/android/server/LooperStatsService.java @@ -31,6 +31,7 @@ import android.text.format.DateFormat; import android.util.KeyValueListParser; import android.util.Slog; +import com.android.internal.os.AppIdToPackageMap; import com.android.internal.os.BackgroundThread; import com.android.internal.os.CachedDeviceState; import com.android.internal.os.LooperStats; @@ -92,6 +93,7 @@ public class LooperStatsService extends Binder { @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; + AppIdToPackageMap packageMap = AppIdToPackageMap.getSnapshot(); pw.print("Start time: "); pw.println(DateFormat.format("yyyy-MM-dd HH:mm:ss", mStats.getStartTimeMillis())); pw.print("On battery time (ms): "); @@ -121,7 +123,7 @@ public class LooperStatsService extends Binder { pw.println(header); for (LooperStats.ExportedEntry entry : entries) { pw.printf("%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", - entry.workSourceUid, + packageMap.mapUid(entry.workSourceUid), entry.threadName, entry.handlerClassName, entry.messageName, diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index b7ed2f9bd473..602aedbc2d00 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -206,45 +206,6 @@ public class Vpn { // Handle of the user initiating VPN. private final int mUserHandle; - // Listen to package removal and change events (update/uninstall) for this user - private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final Uri data = intent.getData(); - final String packageName = data == null ? null : data.getSchemeSpecificPart(); - if (packageName == null) { - return; - } - - synchronized (Vpn.this) { - // Avoid race where always-on package has been unset - if (!packageName.equals(getAlwaysOnPackage())) { - return; - } - - final String action = intent.getAction(); - Log.i(TAG, "Received broadcast " + action + " for always-on VPN package " - + packageName + " in user " + mUserHandle); - - switch(action) { - case Intent.ACTION_PACKAGE_REPLACED: - // Start vpn after app upgrade - startAlwaysOnVpn(); - break; - case Intent.ACTION_PACKAGE_REMOVED: - final boolean isPackageRemoved = !intent.getBooleanExtra( - Intent.EXTRA_REPLACING, false); - if (isPackageRemoved) { - setAlwaysOnPackage(null, false); - } - break; - } - } - } - }; - - private boolean mIsPackageIntentReceiverRegistered = false; - public Vpn(Looper looper, Context context, INetworkManagementService netService, @UserIdInt int userHandle) { this(looper, context, netService, userHandle, new SystemServices(context)); @@ -500,7 +461,6 @@ public class Vpn { // Prepare this app. The notification will update as a side-effect of updateState(). prepareInternal(packageName); } - maybeRegisterPackageChangeReceiverLocked(packageName); setVpnForcedLocked(mLockdown); return true; } @@ -509,31 +469,6 @@ public class Vpn { return packageName == null || VpnConfig.LEGACY_VPN.equals(packageName); } - private void unregisterPackageChangeReceiverLocked() { - if (mIsPackageIntentReceiverRegistered) { - mContext.unregisterReceiver(mPackageIntentReceiver); - mIsPackageIntentReceiverRegistered = false; - } - } - - private void maybeRegisterPackageChangeReceiverLocked(String packageName) { - // Unregister IntentFilter listening for previous always-on package change - unregisterPackageChangeReceiverLocked(); - - if (!isNullOrLegacyVpn(packageName)) { - mIsPackageIntentReceiverRegistered = true; - - IntentFilter intentFilter = new IntentFilter(); - // Protected intent can only be sent by system. No permission required in register. - intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); - intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); - intentFilter.addDataScheme("package"); - intentFilter.addDataSchemeSpecificPart(packageName, PatternMatcher.PATTERN_LITERAL); - mContext.registerReceiverAsUser( - mPackageIntentReceiver, UserHandle.of(mUserHandle), intentFilter, null, null); - } - } - /** * @return the package name of the VPN controller responsible for always-on VPN, * or {@code null} if none is set or always-on VPN is controlled through @@ -1302,7 +1237,6 @@ public class Vpn { setLockdown(false); mAlwaysOn = false; - unregisterPackageChangeReceiverLocked(); // Quit any active connections agentDisconnect(); } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java index 6e08949b634e..26e82704b357 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java @@ -16,13 +16,17 @@ package com.android.server.locksettings.recoverablekeystore.certificate; -import static javax.xml.xpath.XPathConstants.NODESET; - import android.annotation.IntDef; import android.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -40,7 +44,6 @@ import java.security.cert.CertPathBuilderException; import java.security.cert.CertPathValidator; import java.security.cert.CertPathValidatorException; import java.security.cert.CertStore; -import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CollectionCertStoreParameters; @@ -58,15 +61,6 @@ import java.util.Set; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; /** Utility functions related to parsing and validating public-key certificates. */ public final class CertUtils { @@ -167,50 +161,63 @@ public final class CertUtils { static List<String> getXmlNodeContents(@MustExist int mustExist, Element rootNode, String... nodeTags) throws CertParsingException { - String expression = String.join("/", nodeTags); - - XPath xPath = XPathFactory.newInstance().newXPath(); - NodeList nodeList; - try { - nodeList = (NodeList) xPath.compile(expression).evaluate(rootNode, NODESET); - } catch (XPathExpressionException e) { - throw new CertParsingException(e); + if (nodeTags.length == 0) { + throw new CertParsingException("The tag list must not be empty"); } - switch (mustExist) { - case MUST_EXIST_UNENFORCED: - break; - - case MUST_EXIST_EXACTLY_ONE: - if (nodeList.getLength() != 1) { - throw new CertParsingException( - "The XML file must contain exactly one node with the path " - + expression); - } - break; - - case MUST_EXIST_AT_LEAST_ONE: - if (nodeList.getLength() == 0) { - throw new CertParsingException( - "The XML file must contain at least one node with the path " - + expression); - } - break; - - default: - throw new UnsupportedOperationException( - "This value of MustExist is not supported: " + mustExist); + // Go down through all the intermediate node tags (except the last tag for the leaf nodes). + // Note that this implementation requires that at most one path exists for the given + // intermediate node tags. + Element parent = rootNode; + for (int i = 0; i < nodeTags.length - 1; i++) { + String tag = nodeTags[i]; + List<Element> children = getXmlDirectChildren(parent, tag); + if ((children.size() == 0 && mustExist != MUST_EXIST_UNENFORCED) + || children.size() > 1) { + throw new CertParsingException( + "The XML file must contain exactly one path with the tag " + tag); + } + if (children.size() == 0) { + return new ArrayList<>(); + } + parent = children.get(0); } + // Then collect the contents of the leaf nodes. + List<Element> leafs = getXmlDirectChildren(parent, nodeTags[nodeTags.length - 1]); + if (mustExist == MUST_EXIST_EXACTLY_ONE && leafs.size() != 1) { + throw new CertParsingException( + "The XML file must contain exactly one node with the path " + + String.join("/", nodeTags)); + } + if (mustExist == MUST_EXIST_AT_LEAST_ONE && leafs.size() == 0) { + throw new CertParsingException( + "The XML file must contain at least one node with the path " + + String.join("/", nodeTags)); + } List<String> result = new ArrayList<>(); - for (int i = 0; i < nodeList.getLength(); i++) { - Node node = nodeList.item(i); + for (Element leaf : leafs) { // Remove whitespaces and newlines. - result.add(node.getTextContent().replaceAll("\\s", "")); + result.add(leaf.getTextContent().replaceAll("\\s", "")); } return result; } + /** Get the direct child nodes with a given tag. */ + private static List<Element> getXmlDirectChildren(Element parent, String tag) { + // Cannot use Element.getElementsByTagName because it will return all descendant elements + // with the tag name, i.e. not only the direct child nodes. + List<Element> children = new ArrayList<>(); + NodeList childNodes = parent.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals(tag)) { + children.add((Element) node); + } + } + return children; + } + /** * Decodes a base64-encoded string. * diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java index 3a74ab51e9c7..36b7269576b6 100644 --- a/services/core/java/com/android/server/pm/dex/DexManager.java +++ b/services/core/java/com/android/server/pm/dex/DexManager.java @@ -16,6 +16,11 @@ package com.android.server.pm.dex; +import static com.android.server.pm.InstructionSets.getAppDexInstructionSets; +import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo; +import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo; +import static com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode; + import android.content.ContentResolver; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -26,9 +31,9 @@ import android.database.ContentObserver; import android.os.Build; import android.os.FileUtils; import android.os.RemoteException; -import android.os.storage.StorageManager; import android.os.SystemProperties; import android.os.UserHandle; +import android.os.storage.StorageManager; import android.provider.Settings.Global; import android.util.Log; import android.util.Slog; @@ -48,18 +53,14 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Iterator; -import java.util.List; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; -import static com.android.server.pm.InstructionSets.getAppDexInstructionSets; -import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo; -import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo; - /** * This class keeps track of how dex files are used. * Every time it gets a notification about a dex file being loaded it tracks @@ -89,6 +90,12 @@ public class DexManager { // encode and save the dex usage data. private final PackageDexUsage mPackageDexUsage; + // PackageDynamicCodeLoading handles recording of dynamic code loading - + // which is similar to PackageDexUsage but records a different aspect of the data. + // (It additionally includes DEX files loaded with unsupported class loaders, and doesn't + // record class loaders or ISAs.) + private final PackageDynamicCodeLoading mPackageDynamicCodeLoading; + private final IPackageManager mPackageManager; private final PackageDexOptimizer mPackageDexOptimizer; private final Object mInstallLock; @@ -126,14 +133,15 @@ public class DexManager { public DexManager(Context context, IPackageManager pms, PackageDexOptimizer pdo, Installer installer, Object installLock, Listener listener) { - mContext = context; - mPackageCodeLocationsCache = new HashMap<>(); - mPackageDexUsage = new PackageDexUsage(); - mPackageManager = pms; - mPackageDexOptimizer = pdo; - mInstaller = installer; - mInstallLock = installLock; - mListener = listener; + mContext = context; + mPackageCodeLocationsCache = new HashMap<>(); + mPackageDexUsage = new PackageDexUsage(); + mPackageDynamicCodeLoading = new PackageDynamicCodeLoading(); + mPackageManager = pms; + mPackageDexOptimizer = pdo; + mInstaller = installer; + mInstallLock = installLock; + mListener = listener; } public void systemReady() { @@ -207,7 +215,6 @@ public class DexManager { Slog.i(TAG, loadingAppInfo.packageName + " uses unsupported class loader in " + classLoaderNames); } - return; } int dexPathIndex = 0; @@ -236,15 +243,24 @@ public class DexManager { continue; } - // Record dex file usage. If the current usage is a new pattern (e.g. new secondary, - // or UsedByOtherApps), record will return true and we trigger an async write - // to disk to make sure we don't loose the data in case of a reboot. + if (mPackageDynamicCodeLoading.record(searchResult.mOwningPackageName, dexPath, + PackageDynamicCodeLoading.FILE_TYPE_DEX, loaderUserId, + loadingAppInfo.packageName)) { + mPackageDynamicCodeLoading.maybeWriteAsync(); + } + + if (classLoaderContexts != null) { - String classLoaderContext = classLoaderContexts[dexPathIndex]; - if (mPackageDexUsage.record(searchResult.mOwningPackageName, - dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit, - loadingAppInfo.packageName, classLoaderContext)) { - mPackageDexUsage.maybeWriteAsync(); + // Record dex file usage. If the current usage is a new pattern (e.g. new + // secondary, or UsedByOtherApps), record will return true and we trigger an + // async write to disk to make sure we don't loose the data in case of a reboot. + + String classLoaderContext = classLoaderContexts[dexPathIndex]; + if (mPackageDexUsage.record(searchResult.mOwningPackageName, + dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit, + loadingAppInfo.packageName, classLoaderContext)) { + mPackageDexUsage.maybeWriteAsync(); + } } } else { // If we can't find the owner of the dex we simply do not track it. The impact is @@ -268,8 +284,8 @@ public class DexManager { loadInternal(existingPackages); } catch (Exception e) { mPackageDexUsage.clear(); - Slog.w(TAG, "Exception while loading package dex usage. " + - "Starting with a fresh state.", e); + mPackageDynamicCodeLoading.clear(); + Slog.w(TAG, "Exception while loading. Starting with a fresh state.", e); } } @@ -311,15 +327,24 @@ public class DexManager { * all usage information for the package will be removed. */ public void notifyPackageDataDestroyed(String packageName, int userId) { - boolean updated = userId == UserHandle.USER_ALL - ? mPackageDexUsage.removePackage(packageName) - : mPackageDexUsage.removeUserPackage(packageName, userId); // In case there was an update, write the package use info to disk async. - // Note that we do the writing here and not in PackageDexUsage in order to be + // Note that we do the writing here and not in the lower level classes in order to be // consistent with other methods in DexManager (e.g. reconcileSecondaryDexFiles performs // multiple updates in PackageDexUsage before writing it). - if (updated) { - mPackageDexUsage.maybeWriteAsync(); + if (userId == UserHandle.USER_ALL) { + if (mPackageDexUsage.removePackage(packageName)) { + mPackageDexUsage.maybeWriteAsync(); + } + if (mPackageDynamicCodeLoading.removePackage(packageName)) { + mPackageDynamicCodeLoading.maybeWriteAsync(); + } + } else { + if (mPackageDexUsage.removeUserPackage(packageName, userId)) { + mPackageDexUsage.maybeWriteAsync(); + } + if (mPackageDynamicCodeLoading.removeUserPackage(packageName, userId)) { + mPackageDynamicCodeLoading.maybeWriteAsync(); + } } } @@ -388,8 +413,23 @@ public class DexManager { } } - mPackageDexUsage.read(); - mPackageDexUsage.syncData(packageToUsersMap, packageToCodePaths); + try { + mPackageDexUsage.read(); + mPackageDexUsage.syncData(packageToUsersMap, packageToCodePaths); + } catch (Exception e) { + mPackageDexUsage.clear(); + Slog.w(TAG, "Exception while loading package dex usage. " + + "Starting with a fresh state.", e); + } + + try { + mPackageDynamicCodeLoading.read(); + mPackageDynamicCodeLoading.syncData(packageToUsersMap); + } catch (Exception e) { + mPackageDynamicCodeLoading.clear(); + Slog.w(TAG, "Exception while loading package dynamic code usage. " + + "Starting with a fresh state.", e); + } } /** @@ -415,10 +455,16 @@ public class DexManager { * TODO(calin): maybe we should not (prune) so we can have an accurate view when we try * to access the package use. */ + @VisibleForTesting /*package*/ boolean hasInfoOnPackage(String packageName) { return mPackageDexUsage.getPackageUseInfo(packageName) != null; } + @VisibleForTesting + /*package*/ PackageDynamicCode getPackageDynamicCodeInfo(String packageName) { + return mPackageDynamicCodeLoading.getPackageDynamicCodeInfo(packageName); + } + /** * Perform dexopt on with the given {@code options} on the secondary dex files. * @return true if all secondary dex files were processed successfully (compiled or skipped @@ -652,7 +698,7 @@ public class DexManager { // to load dex files through it. try { String dexPathReal = PackageManagerServiceUtils.realpath(new File(dexPath)); - if (dexPathReal != dexPath) { + if (!dexPath.equals(dexPathReal)) { Slog.d(TAG, "Dex loaded with symlink. dexPath=" + dexPath + " dexPathReal=" + dexPathReal); } @@ -675,6 +721,7 @@ public class DexManager { */ public void writePackageDexUsageNow() { mPackageDexUsage.writeNow(); + mPackageDynamicCodeLoading.writeNow(); } private void registerSettingObserver() { diff --git a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java index c2cb861a13fe..f74aa1d69bc8 100644 --- a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java +++ b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java @@ -96,7 +96,7 @@ class PackageDynamicCodeLoading extends AbstractStatsBase<Void> { * @param ownerUserId the user id which runs the code loading the file * @param loadingPackageName the package performing the load * @return whether new information has been recorded - * @throw IllegalArgumentException if clearly invalid information is detected + * @throws IllegalArgumentException if clearly invalid information is detected */ boolean record(String owningPackageName, String filePath, int fileType, int ownerUserId, String loadingPackageName) { diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java index 7683172815e9..aca9702a45c8 100644 --- a/services/core/java/com/android/server/wm/ActivityStack.java +++ b/services/core/java/com/android/server/wm/ActivityStack.java @@ -1797,7 +1797,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai // focus). Also if there is an active pinned stack - we always want to notify it about // task stack changes, because its positioning may depend on it. if (mStackSupervisor.mAppVisibilitiesChangedSinceLastPause - || getDisplay().hasPinnedStack()) { + || (getDisplay() != null && getDisplay().hasPinnedStack())) { mService.getTaskChangeNotificationController().notifyTaskStackChanged(); mStackSupervisor.mAppVisibilitiesChangedSinceLastPause = false; } diff --git a/services/core/java/com/android/server/wm/BarController.java b/services/core/java/com/android/server/wm/BarController.java index a335fa26dfcf..5b20af3534c4 100644 --- a/services/core/java/com/android/server/wm/BarController.java +++ b/services/core/java/com/android/server/wm/BarController.java @@ -219,7 +219,7 @@ public class BarController { } private boolean updateStateLw(final int state) { - if (state != mState) { + if (mWin != null && state != mState) { mState = state; if (DEBUG) Slog.d(mTag, "mState: " + StatusBarManager.windowStateToString(state)); mHandler.post(new Runnable() { diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java index dad7b93e822e..fd07cb046fb5 100644 --- a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java +++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java @@ -18,10 +18,14 @@ package com.android.server.pm.dex; import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo; import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo; +import static com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile; +import static com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -41,6 +45,10 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.pm.Installer; +import dalvik.system.DelegateLastClassLoader; +import dalvik.system.PathClassLoader; +import dalvik.system.VMRuntime; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -50,10 +58,6 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.quality.Strictness; -import dalvik.system.DelegateLastClassLoader; -import dalvik.system.PathClassLoader; -import dalvik.system.VMRuntime; - import java.io.File; import java.util.ArrayList; import java.util.Arrays; @@ -129,6 +133,9 @@ public class DexManagerTests { // Package is not used by others, so we should get nothing back. assertNoUseInfo(mFooUser0); + + // A package loading its own code is not stored as DCL. + assertNoDclInfo(mFooUser0); } @Test @@ -140,6 +147,8 @@ public class DexManagerTests { PackageUseInfo pui = getPackageUseInfo(mBarUser0); assertIsUsedByOtherApps(mBarUser0, pui, true); assertTrue(pui.getDexUseInfoMap().isEmpty()); + + assertHasDclInfo(mBarUser0, mFooUser0, mBarUser0.getBaseAndSplitDexPaths()); } @Test @@ -152,6 +161,8 @@ public class DexManagerTests { assertIsUsedByOtherApps(mFooUser0, pui, false); assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size()); assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0); + + assertHasDclInfo(mFooUser0, mFooUser0, fooSecondaries); } @Test @@ -164,6 +175,8 @@ public class DexManagerTests { assertIsUsedByOtherApps(mBarUser0, pui, false); assertEquals(barSecondaries.size(), pui.getDexUseInfoMap().size()); assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0); + + assertHasDclInfo(mBarUser0, mFooUser0, barSecondaries); } @Test @@ -200,9 +213,10 @@ public class DexManagerTests { } @Test - public void testPackageUseInfoNotFound() { + public void testNoNotify() { // Assert we don't get back data we did not previously record. assertNoUseInfo(mFooUser0); + assertNoDclInfo(mFooUser0); } @Test @@ -210,6 +224,7 @@ public class DexManagerTests { // Notifying with an invalid ISA should be ignored. notifyDexLoad(mInvalidIsa, mInvalidIsa.getSecondaryDexPaths(), mUser0); assertNoUseInfo(mInvalidIsa); + assertNoDclInfo(mInvalidIsa); } @Test @@ -218,6 +233,7 @@ public class DexManagerTests { // register in DexManager#load should be ignored. notifyDexLoad(mDoesNotExist, mDoesNotExist.getBaseAndSplitDexPaths(), mUser0); assertNoUseInfo(mDoesNotExist); + assertNoDclInfo(mDoesNotExist); } @Test @@ -226,6 +242,8 @@ public class DexManagerTests { // Request should be ignored. notifyDexLoad(mBarUser1, mBarUser0.getSecondaryDexPaths(), mUser1); assertNoUseInfo(mBarUser1); + + assertNoDclInfo(mBarUser1); } @Test @@ -235,6 +253,10 @@ public class DexManagerTests { // still check that nothing goes unexpected in DexManager. notifyDexLoad(mBarUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser1); assertNoUseInfo(mBarUser1); + assertNoUseInfo(mFooUser0); + + assertNoDclInfo(mBarUser1); + assertNoDclInfo(mFooUser0); } @Test @@ -247,6 +269,7 @@ public class DexManagerTests { // is trying to load something from it we should not find it. notifyDexLoad(mFooUser0, newSecondaries, mUser0); assertNoUseInfo(newPackage); + assertNoDclInfo(newPackage); // Notify about newPackage install and let mFoo load its dexes. mDexManager.notifyPackageInstalled(newPackage.mPackageInfo, mUser0); @@ -257,6 +280,7 @@ public class DexManagerTests { assertIsUsedByOtherApps(newPackage, pui, false); assertEquals(newSecondaries.size(), pui.getDexUseInfoMap().size()); assertSecondaryUse(newPackage, pui, newSecondaries, /*isUsedByOtherApps*/true, mUser0); + assertHasDclInfo(newPackage, mFooUser0, newSecondaries); } @Test @@ -273,6 +297,7 @@ public class DexManagerTests { assertIsUsedByOtherApps(newPackage, pui, false); assertEquals(newSecondaries.size(), pui.getDexUseInfoMap().size()); assertSecondaryUse(newPackage, pui, newSecondaries, /*isUsedByOtherApps*/false, mUser0); + assertHasDclInfo(newPackage, newPackage, newSecondaries); } @Test @@ -305,6 +330,7 @@ public class DexManagerTests { // We shouldn't find yet the new split as we didn't notify the package update. notifyDexLoad(mFooUser0, newSplits, mUser0); assertNoUseInfo(mBarUser0); + assertNoDclInfo(mBarUser0); // Notify that bar is updated. splitSourceDirs will contain the updated path. mDexManager.notifyPackageUpdated(mBarUser0.getPackageName(), @@ -314,8 +340,8 @@ public class DexManagerTests { // Now, when the split is loaded we will find it and we should mark Bar as usedByOthers. notifyDexLoad(mFooUser0, newSplits, mUser0); PackageUseInfo pui = getPackageUseInfo(mBarUser0); - assertNotNull(pui); assertIsUsedByOtherApps(newSplits, pui, true); + assertHasDclInfo(mBarUser0, mFooUser0, newSplits); } @Test @@ -326,11 +352,15 @@ public class DexManagerTests { mDexManager.notifyPackageDataDestroyed(mBarUser0.getPackageName(), mUser0); - // Bar should not be around since it was removed for all users. + // Data for user 1 should still be present PackageUseInfo pui = getPackageUseInfo(mBarUser1); - assertNotNull(pui); assertSecondaryUse(mBarUser1, pui, mBarUser1.getSecondaryDexPaths(), /*isUsedByOtherApps*/false, mUser1); + assertHasDclInfo(mBarUser1, mBarUser1, mBarUser1.getSecondaryDexPaths()); + + // But not user 0 + assertNoUseInfo(mBarUser0, mUser0); + assertNoDclInfo(mBarUser0, mUser0); } @Test @@ -349,6 +379,8 @@ public class DexManagerTests { PackageUseInfo pui = getPackageUseInfo(mFooUser0); assertIsUsedByOtherApps(mFooUser0, pui, true); assertTrue(pui.getDexUseInfoMap().isEmpty()); + + assertNoDclInfo(mFooUser0); } @Test @@ -362,6 +394,7 @@ public class DexManagerTests { // Foo should not be around since all its secondary dex info were deleted // and it is not used by other apps. assertNoUseInfo(mFooUser0); + assertNoDclInfo(mFooUser0); } @Test @@ -374,6 +407,7 @@ public class DexManagerTests { // Bar should not be around since it was removed for all users. assertNoUseInfo(mBarUser0); + assertNoDclInfo(mBarUser0); } @Test @@ -383,6 +417,7 @@ public class DexManagerTests { notifyDexLoad(mFooUser0, Arrays.asList(frameworkDex), mUser0); // The dex file should not be recognized as a package. assertFalse(mDexManager.hasInfoOnPackage(frameworkDex)); + assertNull(mDexManager.getPackageDynamicCodeInfo(frameworkDex)); } @Test @@ -395,6 +430,8 @@ public class DexManagerTests { assertIsUsedByOtherApps(mFooUser0, pui, false); assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size()); assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0); + + assertHasDclInfo(mFooUser0, mFooUser0, fooSecondaries); } @Test @@ -402,7 +439,12 @@ public class DexManagerTests { List<String> secondaries = mBarUser0UnsupportedClassLoader.getSecondaryDexPaths(); notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0); + // We don't record the dex usage assertNoUseInfo(mBarUser0UnsupportedClassLoader); + + // But we do record this as an intance of dynamic code loading + assertHasDclInfo( + mBarUser0UnsupportedClassLoader, mBarUser0UnsupportedClassLoader, secondaries); } @Test @@ -414,6 +456,8 @@ public class DexManagerTests { notifyDexLoad(mBarUser0, classLoaders, classPaths, mUser0); assertNoUseInfo(mBarUser0); + + assertHasDclInfo(mBarUser0, mBarUser0, mBarUser0.getSecondaryDexPaths()); } @Test @@ -421,6 +465,7 @@ public class DexManagerTests { notifyDexLoad(mBarUser0, null, mUser0); assertNoUseInfo(mBarUser0); + assertNoDclInfo(mBarUser0); } @Test @@ -455,12 +500,14 @@ public class DexManagerTests { notifyDexLoad(mBarUser0, secondaries, mUser0); PackageUseInfo pui = getPackageUseInfo(mBarUser0); assertSecondaryUse(mBarUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0); + assertHasDclInfo(mBarUser0, mBarUser0, secondaries); // Record bar secondaries again with an unsupported class loader. This should not change the // context. notifyDexLoad(mBarUser0UnsupportedClassLoader, secondaries, mUser0); pui = getPackageUseInfo(mBarUser0); assertSecondaryUse(mBarUser0, pui, secondaries, /*isUsedByOtherApps*/false, mUser0); + assertHasDclInfo(mBarUser0, mBarUser0, secondaries); } @Test @@ -533,13 +580,53 @@ public class DexManagerTests { private PackageUseInfo getPackageUseInfo(TestData testData) { assertTrue(mDexManager.hasInfoOnPackage(testData.getPackageName())); - return mDexManager.getPackageUseInfoOrDefault(testData.getPackageName()); + PackageUseInfo pui = mDexManager.getPackageUseInfoOrDefault(testData.getPackageName()); + assertNotNull(pui); + return pui; } private void assertNoUseInfo(TestData testData) { assertFalse(mDexManager.hasInfoOnPackage(testData.getPackageName())); } + private void assertNoUseInfo(TestData testData, int userId) { + if (!mDexManager.hasInfoOnPackage(testData.getPackageName())) { + return; + } + PackageUseInfo pui = getPackageUseInfo(testData); + for (DexUseInfo dexUseInfo : pui.getDexUseInfoMap().values()) { + assertNotEquals(userId, dexUseInfo.getOwnerUserId()); + } + } + + private void assertNoDclInfo(TestData testData) { + assertNull(mDexManager.getPackageDynamicCodeInfo(testData.getPackageName())); + } + + private void assertNoDclInfo(TestData testData, int userId) { + PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(testData.getPackageName()); + if (info == null) { + return; + } + + for (DynamicCodeFile fileInfo : info.mFileUsageMap.values()) { + assertNotEquals(userId, fileInfo.mUserId); + } + } + + private void assertHasDclInfo(TestData owner, TestData loader, List<String> paths) { + PackageDynamicCode info = mDexManager.getPackageDynamicCodeInfo(owner.getPackageName()); + assertNotNull("No DCL data for owner " + owner.getPackageName(), info); + for (String path : paths) { + DynamicCodeFile fileInfo = info.mFileUsageMap.get(path); + assertNotNull("No DCL data for path " + path, fileInfo); + assertEquals(PackageDynamicCodeLoading.FILE_TYPE_DEX, fileInfo.mFileType); + assertEquals(owner.mUserId, fileInfo.mUserId); + assertTrue("No DCL data for loader " + loader.getPackageName(), + fileInfo.mLoadingPackages.contains(loader.getPackageName())); + } + } + private static PackageInfo getMockPackageInfo(String packageName, int userId) { PackageInfo pi = new PackageInfo(); pi.packageName = packageName; @@ -563,11 +650,13 @@ public class DexManagerTests { private final PackageInfo mPackageInfo; private final String mLoaderIsa; private final String mClassLoader; + private final int mUserId; private TestData(String packageName, String loaderIsa, int userId, String classLoader) { mPackageInfo = getMockPackageInfo(packageName, userId); mLoaderIsa = loaderIsa; mClassLoader = classLoader; + mUserId = userId; } private TestData(String packageName, String loaderIsa, int userId) { @@ -603,9 +692,7 @@ public class DexManagerTests { List<String> getBaseAndSplitDexPaths() { List<String> paths = new ArrayList<>(); paths.add(mPackageInfo.applicationInfo.sourceDir); - for (String split : mPackageInfo.applicationInfo.splitSourceDirs) { - paths.add(split); - } + Collections.addAll(paths, mPackageInfo.applicationInfo.splitSourceDirs); return paths; } diff --git a/tools/hiddenapi/exclude.sh b/tools/hiddenapi/exclude.sh index 2291e5a92730..4ffcf6846947 100755 --- a/tools/hiddenapi/exclude.sh +++ b/tools/hiddenapi/exclude.sh @@ -11,6 +11,7 @@ LIBCORE_PACKAGES="\ android.system \ com.android.bouncycastle \ com.android.conscrypt \ + com.android.i18n.phonenumbers \ com.android.okhttp \ com.sun \ dalvik \ |