diff options
98 files changed, 4273 insertions, 2758 deletions
diff --git a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java index 95b614666105..d24487412313 100644 --- a/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/LegacyProtoLogImpl.java @@ -48,7 +48,6 @@ import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; @@ -66,7 +65,7 @@ public class LegacyProtoLogImpl implements IProtoLog { private final String mLegacyViewerConfigFilename; private final TraceBuffer mBuffer; private final LegacyProtoLogViewerConfigReader mViewerConfig; - private final Map<String, IProtoLogGroup> mLogGroups = new TreeMap<>(); + private final TreeMap<String, IProtoLogGroup> mLogGroups; private final Runnable mCacheUpdater; private final int mPerChunkSize; @@ -75,19 +74,20 @@ public class LegacyProtoLogImpl implements IProtoLog { private final Object mProtoLogEnabledLock = new Object(); public LegacyProtoLogImpl(String outputFile, String viewerConfigFilename, - Runnable cacheUpdater) { + TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater) { this(new File(outputFile), viewerConfigFilename, BUFFER_CAPACITY, - new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE, cacheUpdater); + new LegacyProtoLogViewerConfigReader(), PER_CHUNK_SIZE, logGroups, cacheUpdater); } public LegacyProtoLogImpl(File file, String viewerConfigFilename, int bufferCapacity, LegacyProtoLogViewerConfigReader viewerConfig, int perChunkSize, - Runnable cacheUpdater) { + TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater) { mLogFile = file; mBuffer = new TraceBuffer(bufferCapacity); mLegacyViewerConfigFilename = viewerConfigFilename; mViewerConfig = viewerConfig; mPerChunkSize = perChunkSize; + mLogGroups = logGroups; mCacheUpdater = cacheUpdater; } @@ -97,25 +97,21 @@ public class LegacyProtoLogImpl implements IProtoLog { @VisibleForTesting @Override public void log(LogLevel level, IProtoLogGroup group, long messageHash, int paramsMask, - Object[] args) { + @Nullable String messageString, Object[] args) { if (group.isLogToProto()) { logToProto(messageHash, paramsMask, args); } if (group.isLogToLogcat()) { - logToLogcat(group.getTag(), level, messageHash, args); + logToLogcat(group.getTag(), level, messageHash, messageString, args); } } - @Override - public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args) { - // This will be removed very soon so no point implementing it here. - throw new IllegalStateException( - "Not implemented. Only implemented for PerfettoProtoLogImpl."); - } - - private void logToLogcat(String tag, LogLevel level, long messageHash, Object[] args) { + private void logToLogcat(String tag, LogLevel level, long messageHash, + @Nullable String messageString, Object[] args) { String message = null; - final String messageString = mViewerConfig.getViewerString(messageHash); + if (messageString == null) { + messageString = mViewerConfig.getViewerString(messageHash); + } if (messageString != null) { if (args != null) { try { @@ -414,12 +410,5 @@ public class LegacyProtoLogImpl implements IProtoLog { // so we ignore the level argument to this function. return group.isLogToLogcat() || (group.isLogToProto() && isProtoEnabled()); } - - @Override - public void registerGroups(IProtoLogGroup... protoLogGroups) { - for (IProtoLogGroup group : protoLogGroups) { - mLogGroups.put(group.name(), group); - } - } } diff --git a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java b/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java deleted file mode 100644 index 37d09c264c9b..000000000000 --- a/core/java/com/android/internal/protolog/LogcatOnlyProtoLogImpl.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2024 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.protolog; - -import android.text.TextUtils; -import android.util.Log; - -import com.android.internal.protolog.common.ILogger; -import com.android.internal.protolog.common.IProtoLog; -import com.android.internal.protolog.common.IProtoLogGroup; -import com.android.internal.protolog.common.LogLevel; - -/** - * Class only create and used to server temporarily for when there is source code pre-processing by - * the ProtoLog tool, when the tracing to Perfetto flag is off, and the static REQUIRE_PROTOLOGTOOL - * boolean is false. In which case we simply want to log protolog message to logcat. Note, that this - * means that in such cases there is no real advantage of using protolog over logcat. - * - * @deprecated Should not be used. This is just a temporary class to support a legacy behavior. - */ -@Deprecated -public class LogcatOnlyProtoLogImpl implements IProtoLog { - @Override - public void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask, - Object[] args) { - throw new RuntimeException("Not supported when using LogcatOnlyProtoLogImpl"); - } - - @Override - public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object[] args) { - String formattedString = TextUtils.formatSimple(messageString, args); - switch (logLevel) { - case VERBOSE -> Log.v(group.getTag(), formattedString); - case INFO -> Log.i(group.getTag(), formattedString); - case DEBUG -> Log.d(group.getTag(), formattedString); - case WARN -> Log.w(group.getTag(), formattedString); - case ERROR -> Log.e(group.getTag(), formattedString); - case WTF -> Log.wtf(group.getTag(), formattedString); - } - } - - @Override - public boolean isProtoEnabled() { - return false; - } - - @Override - public int startLoggingToLogcat(String[] groups, ILogger logger) { - return 0; - } - - @Override - public int stopLoggingToLogcat(String[] groups, ILogger logger) { - return 0; - } - - @Override - public boolean isEnabled(IProtoLogGroup group, LogLevel level) { - return true; - } - - @Override - public void registerGroups(IProtoLogGroup... protoLogGroups) { - // Does nothing - } -} diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index 5f6766e4926b..42fa6ac0407d 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -42,7 +42,6 @@ import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.SEQ_NEEDS_INCREMENTAL_STATE; import static android.internal.perfetto.protos.TracePacketOuterClass.TracePacket.TIMESTAMP; -import android.annotation.NonNull; import android.annotation.Nullable; import android.internal.perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData; import android.os.ShellCommand; @@ -73,45 +72,37 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; -import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; /** * A service for the ProtoLog logging system. */ public class PerfettoProtoLogImpl implements IProtoLog { private static final String LOG_TAG = "ProtoLog"; - public static final String NULL_STRING = "null"; private final AtomicInteger mTracingInstances = new AtomicInteger(); private final ProtoLogDataSource mDataSource = new ProtoLogDataSource( this::onTracingInstanceStart, - this::onTracingFlush, + this::dumpTransitionTraceConfig, this::onTracingInstanceStop ); private final ProtoLogViewerConfigReader mViewerConfigReader; private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider; - private final TreeMap<String, IProtoLogGroup> mLogGroups = new TreeMap<>(); + private final TreeMap<String, IProtoLogGroup> mLogGroups; private final Runnable mCacheUpdater; - private final int[] mDefaultLogLevelCounts = new int[LogLevel.values().length]; - private final Map<IProtoLogGroup, int[]> mLogLevelCounts = new ArrayMap<>(); - private final Map<IProtoLogGroup, Integer> mCollectStackTraceGroupCounts = new ArrayMap<>(); + private final Map<LogLevel, Integer> mDefaultLogLevelCounts = new ArrayMap<>(); + private final Map<IProtoLogGroup, Map<LogLevel, Integer>> mLogLevelCounts = new ArrayMap<>(); - private final Lock mBackgroundServiceLock = new ReentrantLock(); - private ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor(); + private final ExecutorService mBackgroundLoggingService = Executors.newSingleThreadExecutor(); - public PerfettoProtoLogImpl(String viewerConfigFilePath, Runnable cacheUpdater) { + public PerfettoProtoLogImpl(String viewerConfigFilePath, + TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater) { this(() -> { try { return new ProtoInputStream(new FileInputStream(viewerConfigFilePath)); @@ -119,19 +110,16 @@ public class PerfettoProtoLogImpl implements IProtoLog { Slog.w(LOG_TAG, "Failed to load viewer config file " + viewerConfigFilePath, e); return null; } - }, cacheUpdater); - } - - public PerfettoProtoLogImpl() { - this(null, null, () -> {}); + }, logGroups, cacheUpdater); } public PerfettoProtoLogImpl( ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, + TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater ) { this(viewerConfigInputStreamProvider, - new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider), + new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider), logGroups, cacheUpdater); } @@ -139,6 +127,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { public PerfettoProtoLogImpl( ViewerConfigInputStreamProvider viewerConfigInputStreamProvider, ProtoLogViewerConfigReader viewerConfigReader, + TreeMap<String, IProtoLogGroup> logGroups, Runnable cacheUpdater ) { Producer.init(InitArguments.DEFAULTS); @@ -151,6 +140,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { mDataSource.register(params); this.mViewerConfigInputStreamProvider = viewerConfigInputStreamProvider; this.mViewerConfigReader = viewerConfigReader; + this.mLogGroups = logGroups; this.mCacheUpdater = cacheUpdater; } @@ -159,70 +149,23 @@ public class PerfettoProtoLogImpl implements IProtoLog { */ @VisibleForTesting @Override - public void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask, - @Nullable Object[] args) { - log(logLevel, group, new Message(messageHash, paramsMask), args); - } - - @Override - public void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args) { - log(logLevel, group, new Message(messageString), args); - } - - private void log(LogLevel logLevel, IProtoLogGroup group, Message message, - @Nullable Object[] args) { - if (isProtoEnabled()) { - long tsNanos = SystemClock.elapsedRealtimeNanos(); - final String stacktrace; - if (mCollectStackTraceGroupCounts.getOrDefault(group, 0) > 0) { - stacktrace = collectStackTrace(); - } else { - stacktrace = null; - } - try { - mBackgroundServiceLock.lock(); - mBackgroundLoggingService.execute(() -> - logToProto(logLevel, group, message, args, tsNanos, - stacktrace)); - } finally { - mBackgroundServiceLock.unlock(); - } - } - if (group.isLogToLogcat()) { - logToLogcat(group.getTag(), logLevel, message, args); - } - } - - private void onTracingFlush() { - final ExecutorService loggingService; - try { - mBackgroundServiceLock.lock(); - loggingService = mBackgroundLoggingService; - mBackgroundLoggingService = Executors.newSingleThreadExecutor(); - } finally { - mBackgroundServiceLock.unlock(); - } + public void log(LogLevel level, IProtoLogGroup group, long messageHash, int paramsMask, + @Nullable String messageString, Object[] args) { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "log"); + long tsNanos = SystemClock.elapsedRealtimeNanos(); try { - loggingService.shutdown(); - boolean finished = loggingService.awaitTermination(10, TimeUnit.SECONDS); - - if (!finished) { - Log.e(LOG_TAG, "ProtoLog background tracing service didn't finish gracefully."); + mBackgroundLoggingService.submit(() -> + logToProto(level, group.name(), messageHash, paramsMask, args, tsNanos)); + if (group.isLogToLogcat()) { + logToLogcat(group.getTag(), level, messageHash, messageString, args); } - } catch (InterruptedException e) { - Log.e(LOG_TAG, "Failed to wait for tracing to finish", e); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } - - dumpTransitionTraceConfig(); } private void dumpTransitionTraceConfig() { - if (mViewerConfigInputStreamProvider == null) { - // No viewer config available - return; - } - ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream(); if (pis == null) { @@ -313,53 +256,39 @@ public class PerfettoProtoLogImpl implements IProtoLog { os.end(outMessagesToken); } - private void logToLogcat(String tag, LogLevel level, Message message, - @Nullable Object[] args) { - String messageString = message.getMessage(mViewerConfigReader); - - if (messageString == null) { - StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE"); - if (args != null) { - builder.append(" args = ("); - builder.append(String.join(", ", Arrays.stream(args) - .map(it -> { - if (it == null) { - return "null"; - } else { - return it.toString(); - } - }).toList())); - builder.append(")"); - } - messageString = builder.toString(); - args = new Object[0]; - } - - logToLogcat(tag, level, messageString, args); - } - - private void logToLogcat(String tag, LogLevel level, String message, @Nullable Object[] args) { + private void logToLogcat(String tag, LogLevel level, long messageHash, + @Nullable String messageString, Object[] args) { Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logToLogcat"); try { - doLogToLogcat(tag, level, message, args); + doLogToLogcat(tag, level, messageHash, messageString, args); } finally { Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } } - private void doLogToLogcat(String tag, LogLevel level, @NonNull String messageString, - @Nullable Object[] args) { - String message; - if (args != null) { - try { - message = TextUtils.formatSimple(messageString, args); - } catch (IllegalArgumentException e) { - message = "FORMAT_ERROR \"" + messageString + "\", args=(" - + String.join( - ", ", Arrays.stream(args).map(Object::toString).toList()) + ")"; + private void doLogToLogcat(String tag, LogLevel level, long messageHash, + @androidx.annotation.Nullable String messageString, Object[] args) { + String message = null; + if (messageString == null) { + messageString = mViewerConfigReader.getViewerString(messageHash); + } + if (messageString != null) { + if (args != null) { + try { + message = TextUtils.formatSimple(messageString, args); + } catch (Exception ex) { + Slog.w(LOG_TAG, "Invalid ProtoLog format string.", ex); + } + } else { + message = messageString; } - } else { - message = messageString; + } + if (message == null) { + StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE (" + messageHash + ")"); + for (Object o : args) { + builder.append(" ").append(o); + } + message = builder.toString(); } passToLogcat(tag, level, message); } @@ -391,21 +320,25 @@ public class PerfettoProtoLogImpl implements IProtoLog { } } - private void logToProto(LogLevel level, IProtoLogGroup logGroup, Message message, Object[] args, - long tsNanos, @Nullable String stacktrace) { + private void logToProto(LogLevel level, String groupName, long messageHash, int paramsMask, + Object[] args, long tsNanos) { + if (!isProtoEnabled()) { + return; + } + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "logToProto"); try { - doLogToProto(level, logGroup, message, args, tsNanos, stacktrace); + doLogToProto(level, groupName, messageHash, paramsMask, args, tsNanos); } finally { Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } } - private void doLogToProto(LogLevel level, IProtoLogGroup logGroup, Message message, - Object[] args, long tsNanos, @Nullable String stacktrace) { + private void doLogToProto(LogLevel level, String groupName, long messageHash, int paramsMask, + Object[] args, long tsNanos) { mDataSource.trace(ctx -> { final ProtoLogDataSource.TlsState tlsState = ctx.getCustomTlsState(); - final LogLevel logFrom = tlsState.getLogFromLevel(logGroup.name()); + final LogLevel logFrom = tlsState.getLogFromLevel(groupName); if (level.ordinal() < logFrom.ordinal()) { return; @@ -417,44 +350,30 @@ public class PerfettoProtoLogImpl implements IProtoLog { // trace processing easier. int argIndex = 0; for (Object o : args) { - int type = LogDataType.bitmaskToLogDataType(message.getMessageMask(), argIndex); + int type = LogDataType.bitmaskToLogDataType(paramsMask, argIndex); if (type == LogDataType.STRING) { - if (o == null) { - internStringArg(ctx, NULL_STRING); - } else { - internStringArg(ctx, o.toString()); - } + internStringArg(ctx, o.toString()); } argIndex++; } } int internedStacktrace = 0; - if (tlsState.getShouldCollectStacktrace(logGroup.name())) { + if (tlsState.getShouldCollectStacktrace(groupName)) { // Intern stackstraces before creating the trace packet for the proto message so // that the interned stacktrace strings appear before in the trace to make the // trace processing easier. + String stacktrace = collectStackTrace(); internedStacktrace = internStacktraceString(ctx, stacktrace); } - boolean needsIncrementalState = false; - - long messageHash = 0; - if (message.mMessageHash != null) { - messageHash = message.mMessageHash; - } - if (message.mMessageString != null) { - needsIncrementalState = true; - messageHash = - internProtoMessage(ctx, level, logGroup, message.mMessageString); - } - final ProtoOutputStream os = ctx.newTracePacket(); os.write(TIMESTAMP, tsNanos); long token = os.start(PROTOLOG_MESSAGE); - os.write(MESSAGE_ID, messageHash); + boolean needsIncrementalState = false; + if (args != null) { int argIndex = 0; @@ -462,39 +381,22 @@ public class PerfettoProtoLogImpl implements IProtoLog { ArrayList<Double> doubleParams = new ArrayList<>(); ArrayList<Boolean> booleanParams = new ArrayList<>(); for (Object o : args) { - int type = LogDataType.bitmaskToLogDataType(message.getMessageMask(), argIndex); + int type = LogDataType.bitmaskToLogDataType(paramsMask, argIndex); try { switch (type) { case LogDataType.STRING: - final int internedStringId; - if (o == null) { - internedStringId = internStringArg(ctx, NULL_STRING); - } else { - internedStringId = internStringArg(ctx, o.toString()); - } + final int internedStringId = internStringArg(ctx, o.toString()); os.write(STR_PARAM_IIDS, internedStringId); needsIncrementalState = true; break; case LogDataType.LONG: - if (o == null) { - longParams.add(0); - } else { - longParams.add(((Number) o).longValue()); - } + longParams.add(((Number) o).longValue()); break; case LogDataType.DOUBLE: - if (o == null) { - doubleParams.add(0d); - } else { - doubleParams.add(((Number) o).doubleValue()); - } + doubleParams.add(((Number) o).doubleValue()); break; case LogDataType.BOOLEAN: - if (o == null) { - booleanParams.add(false); - } else { - booleanParams.add((boolean) o); - } + booleanParams.add((boolean) o); break; } } catch (ClassCastException ex) { @@ -512,7 +414,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { booleanParams.forEach(it -> os.write(BOOLEAN_PARAMS, it ? 1 : 0)); } - if (tlsState.getShouldCollectStacktrace(logGroup.name())) { + if (tlsState.getShouldCollectStacktrace(groupName)) { os.write(STACKTRACE_IID, internedStacktrace); } @@ -525,63 +427,6 @@ public class PerfettoProtoLogImpl implements IProtoLog { }); } - private long internProtoMessage( - TracingContext<ProtoLogDataSource.Instance, ProtoLogDataSource.TlsState, - ProtoLogDataSource.IncrementalState> ctx, LogLevel level, - IProtoLogGroup logGroup, String message) { - final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState(); - - if (!incrementalState.clearReported) { - final ProtoOutputStream os = ctx.newTracePacket(); - os.write(SEQUENCE_FLAGS, SEQ_INCREMENTAL_STATE_CLEARED); - incrementalState.clearReported = true; - } - - - if (!incrementalState.protologGroupInterningSet.contains(logGroup.getId())) { - incrementalState.protologGroupInterningSet.add(logGroup.getId()); - - final ProtoOutputStream os = ctx.newTracePacket(); - final long protologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG); - final long groupConfigToken = os.start(GROUPS); - - os.write(ID, logGroup.getId()); - os.write(NAME, logGroup.name()); - os.write(TAG, logGroup.getTag()); - - os.end(groupConfigToken); - os.end(protologViewerConfigToken); - } - - final Long messageHash = hash(level, logGroup.name(), message); - if (!incrementalState.protologMessageInterningSet.contains(messageHash)) { - incrementalState.protologMessageInterningSet.add(messageHash); - - final ProtoOutputStream os = ctx.newTracePacket(); - final long protologViewerConfigToken = os.start(PROTOLOG_VIEWER_CONFIG); - final long messageConfigToken = os.start(MESSAGES); - - os.write(MessageData.MESSAGE_ID, messageHash); - os.write(MESSAGE, message); - os.write(LEVEL, level.ordinal()); - os.write(GROUP_ID, logGroup.getId()); - - os.end(messageConfigToken); - os.end(protologViewerConfigToken); - } - - return messageHash; - } - - private Long hash( - LogLevel logLevel, - String logGroup, - String messageString - ) { - final String fullStringIdentifier = messageString + logLevel + logGroup; - return UUID.nameUUIDFromBytes(fullStringIdentifier.getBytes()).getMostSignificantBits(); - } - private static final int STACK_SIZE_TO_PROTO_LOG_ENTRY_CALL = 12; private String collectStackTrace() { @@ -621,7 +466,7 @@ public class PerfettoProtoLogImpl implements IProtoLog { ProtoLogDataSource.IncrementalState> ctx, Map<String, Integer> internMap, long fieldId, - @NonNull String string + String string ) { final ProtoLogDataSource.IncrementalState incrementalState = ctx.getIncrementalState(); @@ -678,17 +523,25 @@ public class PerfettoProtoLogImpl implements IProtoLog { @Override public boolean isEnabled(IProtoLogGroup group, LogLevel level) { - final int[] groupLevelCount = mLogLevelCounts.get(group); - return (groupLevelCount == null && mDefaultLogLevelCounts[level.ordinal()] > 0) - || (groupLevelCount != null && groupLevelCount[level.ordinal()] > 0) - || group.isLogToLogcat(); + return group.isLogToLogcat() || getLogFromLevel(group).ordinal() <= level.ordinal(); } - @Override - public void registerGroups(IProtoLogGroup... protoLogGroups) { - for (IProtoLogGroup protoLogGroup : protoLogGroups) { - mLogGroups.put(protoLogGroup.name(), protoLogGroup); + private LogLevel getLogFromLevel(IProtoLogGroup group) { + if (mLogLevelCounts.containsKey(group)) { + for (LogLevel logLevel : LogLevel.values()) { + if (mLogLevelCounts.get(group).getOrDefault(logLevel, 0) > 0) { + return logLevel; + } + } + } else { + for (LogLevel logLevel : LogLevel.values()) { + if (mDefaultLogLevelCounts.getOrDefault(logLevel, 0) > 0) { + return logLevel; + } + } } + + return LogLevel.WTF; } /** @@ -767,51 +620,36 @@ public class PerfettoProtoLogImpl implements IProtoLog { } private synchronized void onTracingInstanceStart(ProtoLogDataSource.ProtoLogConfig config) { + this.mTracingInstances.incrementAndGet(); + final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; - for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) { - mDefaultLogLevelCounts[i]++; - } + mDefaultLogLevelCounts.put(defaultLogFrom, + mDefaultLogLevelCounts.getOrDefault(defaultLogFrom, 0) + 1); final Set<String> overriddenGroupTags = config.getGroupTagsWithOverriddenConfigs(); for (String overriddenGroupTag : overriddenGroupTags) { IProtoLogGroup group = mLogGroups.get(overriddenGroupTag); - if (group == null) { - throw new IllegalArgumentException("Trying to set config for \"" - + overriddenGroupTag + "\" that isn't registered"); - } - - mLogLevelCounts.putIfAbsent(group, new int[LogLevel.values().length]); - final int[] logLevelsCountsForGroup = mLogLevelCounts.get(group); + mLogLevelCounts.putIfAbsent(group, new ArrayMap<>()); + final Map<LogLevel, Integer> logLevelsCountsForGroup = mLogLevelCounts.get(group); final LogLevel logFromLevel = config.getConfigFor(overriddenGroupTag).logFrom; - for (int i = logFromLevel.ordinal(); i < LogLevel.values().length; i++) { - logLevelsCountsForGroup[i]++; - } - - if (config.getConfigFor(overriddenGroupTag).collectStackTrace) { - mCollectStackTraceGroupCounts.put(group, - mCollectStackTraceGroupCounts.getOrDefault(group, 0) + 1); - } - - if (config.getConfigFor(overriddenGroupTag).collectStackTrace) { - mCollectStackTraceGroupCounts.put(group, - mCollectStackTraceGroupCounts.getOrDefault(group, 0) + 1); - } + logLevelsCountsForGroup.put(logFromLevel, + logLevelsCountsForGroup.getOrDefault(logFromLevel, 0) + 1); } mCacheUpdater.run(); - - this.mTracingInstances.incrementAndGet(); } private synchronized void onTracingInstanceStop(ProtoLogDataSource.ProtoLogConfig config) { this.mTracingInstances.decrementAndGet(); final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom; - for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) { - mDefaultLogLevelCounts[i]--; + mDefaultLogLevelCounts.put(defaultLogFrom, + mDefaultLogLevelCounts.get(defaultLogFrom) - 1); + if (mDefaultLogLevelCounts.get(defaultLogFrom) <= 0) { + mDefaultLogLevelCounts.remove(defaultLogFrom); } final Set<String> overriddenGroupTags = config.getGroupTagsWithOverriddenConfigs(); @@ -819,24 +657,18 @@ public class PerfettoProtoLogImpl implements IProtoLog { for (String overriddenGroupTag : overriddenGroupTags) { IProtoLogGroup group = mLogGroups.get(overriddenGroupTag); - final int[] logLevelsCountsForGroup = mLogLevelCounts.get(group); + mLogLevelCounts.putIfAbsent(group, new ArrayMap<>()); + final Map<LogLevel, Integer> logLevelsCountsForGroup = mLogLevelCounts.get(group); final LogLevel logFromLevel = config.getConfigFor(overriddenGroupTag).logFrom; - for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) { - logLevelsCountsForGroup[i]--; + logLevelsCountsForGroup.put(logFromLevel, + logLevelsCountsForGroup.get(logFromLevel) - 1); + if (logLevelsCountsForGroup.get(logFromLevel) <= 0) { + logLevelsCountsForGroup.remove(logFromLevel); } - if (Arrays.stream(logLevelsCountsForGroup).allMatch(it -> it == 0)) { + if (logLevelsCountsForGroup.isEmpty()) { mLogLevelCounts.remove(group); } - - if (config.getConfigFor(overriddenGroupTag).collectStackTrace) { - mCollectStackTraceGroupCounts.put(group, - mCollectStackTraceGroupCounts.get(group) - 1); - - if (mCollectStackTraceGroupCounts.get(group) == 0) { - mCollectStackTraceGroupCounts.remove(group); - } - } } mCacheUpdater.run(); @@ -849,36 +681,5 @@ public class PerfettoProtoLogImpl implements IProtoLog { pw.flush(); } } - - private static class Message { - private final Long mMessageHash; - private final Integer mMessageMask; - private final String mMessageString; - - private Message(Long messageHash, int messageMask) { - this.mMessageHash = messageHash; - this.mMessageMask = messageMask; - this.mMessageString = null; - } - - private Message(String messageString) { - this.mMessageHash = null; - final List<Integer> argTypes = LogDataType.parseFormatString(messageString); - this.mMessageMask = LogDataType.logDataTypesToBitMask(argTypes); - this.mMessageString = messageString; - } - - private int getMessageMask() { - return mMessageMask; - } - - private String getMessage(ProtoLogViewerConfigReader viewerConfigReader) { - if (mMessageString != null) { - return mMessageString; - } - - return viewerConfigReader.getViewerString(mMessageHash); - } - } } diff --git a/core/java/com/android/internal/protolog/ProtoLog.java b/core/java/com/android/internal/protolog/ProtoLog.java index 99d441812fa8..0118c056d682 100644 --- a/core/java/com/android/internal/protolog/ProtoLog.java +++ b/core/java/com/android/internal/protolog/ProtoLog.java @@ -44,23 +44,21 @@ public class ProtoLog { // LINT.ThenChange(frameworks/base/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt) // Needs to be set directly otherwise the protologtool tries to transform the method call - @Deprecated public static boolean REQUIRE_PROTOLOGTOOL = true; - private static IProtoLog sProtoLogInstance; - /** * DEBUG level log. * * @param group {@code IProtoLogGroup} controlling this log call. * @param messageString constant format string for the logged message. * @param args parameters to be used with the format string. - * - * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is - * executed. Check generated code for actual call. */ public static void d(IProtoLogGroup group, String messageString, Object... args) { - logStringMessage(LogLevel.DEBUG, group, messageString, args); + // Stub, replaced by the ProtoLogTool. + if (REQUIRE_PROTOLOGTOOL) { + throw new UnsupportedOperationException( + "ProtoLog calls MUST be processed with ProtoLogTool"); + } } /** @@ -69,12 +67,13 @@ public class ProtoLog { * @param group {@code IProtoLogGroup} controlling this log call. * @param messageString constant format string for the logged message. * @param args parameters to be used with the format string. - * - * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is - * executed. Check generated code for actual call. */ public static void v(IProtoLogGroup group, String messageString, Object... args) { - logStringMessage(LogLevel.VERBOSE, group, messageString, args); + // Stub, replaced by the ProtoLogTool. + if (REQUIRE_PROTOLOGTOOL) { + throw new UnsupportedOperationException( + "ProtoLog calls MUST be processed with ProtoLogTool"); + } } /** @@ -83,12 +82,13 @@ public class ProtoLog { * @param group {@code IProtoLogGroup} controlling this log call. * @param messageString constant format string for the logged message. * @param args parameters to be used with the format string. - * - * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is - * executed. Check generated code for actual call. */ public static void i(IProtoLogGroup group, String messageString, Object... args) { - logStringMessage(LogLevel.INFO, group, messageString, args); + // Stub, replaced by the ProtoLogTool. + if (REQUIRE_PROTOLOGTOOL) { + throw new UnsupportedOperationException( + "ProtoLog calls MUST be processed with ProtoLogTool"); + } } /** @@ -97,12 +97,13 @@ public class ProtoLog { * @param group {@code IProtoLogGroup} controlling this log call. * @param messageString constant format string for the logged message. * @param args parameters to be used with the format string. - * - * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is - * executed. Check generated code for actual call. */ public static void w(IProtoLogGroup group, String messageString, Object... args) { - logStringMessage(LogLevel.WARN, group, messageString, args); + // Stub, replaced by the ProtoLogTool. + if (REQUIRE_PROTOLOGTOOL) { + throw new UnsupportedOperationException( + "ProtoLog calls MUST be processed with ProtoLogTool"); + } } /** @@ -111,12 +112,13 @@ public class ProtoLog { * @param group {@code IProtoLogGroup} controlling this log call. * @param messageString constant format string for the logged message. * @param args parameters to be used with the format string. - * - * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is - * executed. Check generated code for actual call. */ public static void e(IProtoLogGroup group, String messageString, Object... args) { - logStringMessage(LogLevel.ERROR, group, messageString, args); + // Stub, replaced by the ProtoLogTool. + if (REQUIRE_PROTOLOGTOOL) { + throw new UnsupportedOperationException( + "ProtoLog calls MUST be processed with ProtoLogTool"); + } } /** @@ -125,12 +127,13 @@ public class ProtoLog { * @param group {@code IProtoLogGroup} controlling this log call. * @param messageString constant format string for the logged message. * @param args parameters to be used with the format string. - * - * NOTE: If source code is pre-processed by ProtoLogTool this is not the function call that is - * executed. Check generated code for actual call. */ public static void wtf(IProtoLogGroup group, String messageString, Object... args) { - logStringMessage(LogLevel.WTF, group, messageString, args); + // Stub, replaced by the ProtoLogTool. + if (REQUIRE_PROTOLOGTOOL) { + throw new UnsupportedOperationException( + "ProtoLog calls MUST be processed with ProtoLogTool"); + } } /** @@ -139,7 +142,11 @@ public class ProtoLog { * @return true iff this is being logged. */ public static boolean isEnabled(IProtoLogGroup group, LogLevel level) { - return sProtoLogInstance.isEnabled(group, level); + if (REQUIRE_PROTOLOGTOOL) { + throw new UnsupportedOperationException( + "ProtoLog calls MUST be processed with ProtoLogTool"); + } + return false; } /** @@ -147,38 +154,10 @@ public class ProtoLog { * @return A singleton instance of ProtoLog. */ public static IProtoLog getSingleInstance() { - return sProtoLogInstance; - } - - /** - * Registers available protolog groups. A group must be registered before it can be used. - * @param protoLogGroups The groups to register for use in protolog. - */ - public static void registerGroups(IProtoLogGroup... protoLogGroups) { - sProtoLogInstance.registerGroups(protoLogGroups); - } - - private static void logStringMessage(LogLevel logLevel, IProtoLogGroup group, - String stringMessage, Object... args) { - if (sProtoLogInstance == null) { - throw new IllegalStateException( - "Trying to use ProtoLog before it is initialized in this process."); - } - - if (sProtoLogInstance.isEnabled(group, logLevel)) { - sProtoLogInstance.log(logLevel, group, stringMessage, args); - } - } - - static { - if (android.tracing.Flags.perfettoProtologTracing()) { - sProtoLogInstance = new PerfettoProtoLogImpl(); - } else { - if (REQUIRE_PROTOLOGTOOL) { - throw new RuntimeException("REQUIRE_PROTOLOGTOOL not set to false."); - } else { - sProtoLogInstance = new LogcatOnlyProtoLogImpl(); - } + if (REQUIRE_PROTOLOGTOOL) { + throw new UnsupportedOperationException( + "ProtoLog calls MUST be processed with ProtoLogTool"); } + return null; } } diff --git a/core/java/com/android/internal/protolog/ProtoLogDataSource.java b/core/java/com/android/internal/protolog/ProtoLogDataSource.java index 84f3237142b8..6ab79b92784e 100644 --- a/core/java/com/android/internal/protolog/ProtoLogDataSource.java +++ b/core/java/com/android/internal/protolog/ProtoLogDataSource.java @@ -40,7 +40,6 @@ import com.android.internal.protolog.common.LogLevel; import java.io.IOException; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.Consumer; @@ -139,8 +138,6 @@ public class ProtoLogDataSource extends DataSource<ProtoLogDataSource.Instance, } public static class IncrementalState { - public final Set<Integer> protologGroupInterningSet = new HashSet<>(); - public final Set<Long> protologMessageInterningSet = new HashSet<>(); public final Map<String, Integer> argumentInterningMap = new HashMap<>(); public final Map<String, Integer> stacktraceInterningMap = new HashMap<>(); public boolean clearReported = false; diff --git a/core/java/com/android/internal/protolog/ProtoLogImpl.java b/core/java/com/android/internal/protolog/ProtoLogImpl.java index 3082295a522c..6d142afce626 100644 --- a/core/java/com/android/internal/protolog/ProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogImpl.java @@ -54,33 +54,48 @@ public class ProtoLogImpl { private static Runnable sCacheUpdater; /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ - public static void d(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) { - getSingleInstance().log(LogLevel.DEBUG, group, messageHash, paramsMask, args); + public static void d(IProtoLogGroup group, long messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance() + .log(LogLevel.DEBUG, group, messageHash, paramsMask, messageString, args); } /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ - public static void v(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) { - getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, args); + public static void v(IProtoLogGroup group, long messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, messageString, + args); } /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ - public static void i(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) { - getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, args); + public static void i(IProtoLogGroup group, long messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args); } /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ - public static void w(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) { - getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, args); + public static void w(IProtoLogGroup group, long messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, messageString, args); } /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ - public static void e(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) { - getSingleInstance().log(LogLevel.ERROR, group, messageHash, paramsMask, args); + public static void e(IProtoLogGroup group, long messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance() + .log(LogLevel.ERROR, group, messageHash, paramsMask, messageString, args); } /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */ - public static void wtf(IProtoLogGroup group, long messageHash, int paramsMask, Object... args) { - getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, args); + public static void wtf(IProtoLogGroup group, long messageHash, int paramsMask, + @Nullable String messageString, + Object... args) { + getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args); } /** @@ -92,27 +107,18 @@ public class ProtoLogImpl { } /** - * Registers available protolog groups. A group must be registered before it can be used. - * @param protoLogGroups The groups to register for use in protolog. - */ - public static void registerGroups(IProtoLogGroup... protoLogGroups) { - getSingleInstance().registerGroups(protoLogGroups); - } - - /** * Returns the single instance of the ProtoLogImpl singleton class. */ public static synchronized IProtoLog getSingleInstance() { if (sServiceInstance == null) { if (android.tracing.Flags.perfettoProtologTracing()) { - sServiceInstance = new PerfettoProtoLogImpl(sViewerConfigPath, sCacheUpdater); + sServiceInstance = new PerfettoProtoLogImpl( + sViewerConfigPath, sLogGroups, sCacheUpdater); } else { sServiceInstance = new LegacyProtoLogImpl( - sLegacyOutputFilePath, sLegacyViewerConfigPath, sCacheUpdater); + sLegacyOutputFilePath, sLegacyViewerConfigPath, sLogGroups, sCacheUpdater); } - IProtoLogGroup[] groups = sLogGroups.values().toArray(new IProtoLogGroup[0]); - sServiceInstance.registerGroups(groups); sCacheUpdater.run(); } return sServiceInstance; diff --git a/core/java/com/android/internal/protolog/common/IProtoLog.java b/core/java/com/android/internal/protolog/common/IProtoLog.java index f5695acd0614..f72d9f79958d 100644 --- a/core/java/com/android/internal/protolog/common/IProtoLog.java +++ b/core/java/com/android/internal/protolog/common/IProtoLog.java @@ -27,19 +27,11 @@ public interface IProtoLog { * @param group The group this message belongs to. * @param messageHash The hash of the message. * @param paramsMask The parameters mask of the message. - * @param args The arguments of the message. - */ - void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask, - Object[] args); - - /** - * Log a ProtoLog message - * @param logLevel Log level of the proto message. - * @param group The group this message belongs to. * @param messageString The message string. * @param args The arguments of the message. */ - void log(LogLevel logLevel, IProtoLogGroup group, String messageString, Object... args); + void log(LogLevel logLevel, IProtoLogGroup group, long messageHash, int paramsMask, + String messageString, Object[] args); /** * Check if ProtoLog is tracing. @@ -68,10 +60,4 @@ public interface IProtoLog { * @return If we need to log this group and level to either ProtoLog or Logcat. */ boolean isEnabled(IProtoLogGroup group, LogLevel level); - - /** - * Registers available protolog groups. A group must be registered before it can be used. - * @param protoLogGroups The groups to register for use in protolog. - */ - void registerGroups(IProtoLogGroup... protoLogGroups); } diff --git a/core/java/com/android/internal/widget/OWNERS b/core/java/com/android/internal/widget/OWNERS index e2672f5b03ba..cf2f202a03ac 100644 --- a/core/java/com/android/internal/widget/OWNERS +++ b/core/java/com/android/internal/widget/OWNERS @@ -9,18 +9,18 @@ per-file *Lockscreen* = file:/services/core/java/com/android/server/locksettings per-file *LockSettings* = file:/services/core/java/com/android/server/locksettings/OWNERS # Notification related -per-file *Notification* = file:/services/core/java/com/android/server/notification/OWNERS -per-file *Messaging* = file:/services/core/java/com/android/server/notification/OWNERS -per-file *Message* = file:/services/core/java/com/android/server/notification/OWNERS -per-file *Conversation* = file:/services/core/java/com/android/server/notification/OWNERS -per-file *People* = file:/services/core/java/com/android/server/notification/OWNERS -per-file *ImageResolver* = file:/services/core/java/com/android/server/notification/OWNERS -per-file CallLayout.java = file:/services/core/java/com/android/server/notification/OWNERS -per-file CachingIconView.java = file:/services/core/java/com/android/server/notification/OWNERS -per-file ImageFloatingTextView.java = file:/services/core/java/com/android/server/notification/OWNERS -per-file ObservableTextView.java = file:/services/core/java/com/android/server/notification/OWNERS -per-file RemeasuringLinearLayout.java = file:/services/core/java/com/android/server/notification/OWNERS -per-file ViewClippingUtil.java = file:/services/core/java/com/android/server/notification/OWNERS +per-file *Notification* = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file *Messaging* = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file *Message* = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file *Conversation* = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file *People* = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file *ImageResolver* = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file CallLayout.java = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file CachingIconView.java = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file ImageFloatingTextView.java = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file ObservableTextView.java = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file RemeasuringLinearLayout.java = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS +per-file ViewClippingUtil.java = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS # Appwidget related per-file *RemoteViews* = file:/services/appwidget/java/com/android/server/appwidget/OWNERS diff --git a/core/res/res/values-watch/colors.xml b/core/res/res/values-watch/colors.xml deleted file mode 100644 index e2b7505f3c6e..000000000000 --- a/core/res/res/values-watch/colors.xml +++ /dev/null @@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2014 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. ---> - -<!-- Watch specific system colors. --> -<resources> - <color name="system_error_light">#B3261E</color> - <color name="system_on_error_light">#FFFFFF</color> - <color name="system_error_container_light">#F7DCDA</color> - <color name="system_on_error_container_light">#410E0B</color> - - <color name="system_error_dark">#F2B8B5</color> - <color name="system_on_error_dark">#601410</color> - <color name="system_error_container_dark">#FF8986</color> - <color name="system_on_error_container_dark">#410E0B</color> - - <!-- With material deprecation of 'background' in favor of 'surface' we flatten these - on watches to match the black background requirements --> - <color name="system_surface_dark">#000000</color> - <color name="system_surface_dim_dark">#000000</color> - <color name="system_surface_bright_dark">#000000</color> - - <!-- Wear flattens the typical 5 container layers to 3; container + high & low --> - <color name="system_surface_container_dark">#303030</color> - <color name="system_surface_variant_dark">#303030</color> - <color name="system_surface_container_high_dark">#474747</color> - <color name="system_surface_container_highest_dark">#474747</color> - <color name="system_surface_container_low_dark">#252626</color> - <color name="system_surface_container_lowest_dark">#252626</color> - -</resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 66e1d7af6311..aa499d9ee8a0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -585,9 +585,10 @@ public abstract class WMShellModule { @Provides static ExitDesktopTaskTransitionHandler provideExitDesktopTaskTransitionHandler( Transitions transitions, - Context context - ) { - return new ExitDesktopTaskTransitionHandler(transitions, context); + Context context, + InteractionJankMonitor interactionJankMonitor) { + return new ExitDesktopTaskTransitionHandler( + transitions, context, interactionJankMonitor); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java index 891f75cfdbda..171378f9a164 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -42,6 +42,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.Cuj; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; @@ -60,6 +62,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH private final Context mContext; private final Transitions mTransitions; + private final InteractionJankMonitor mInteractionJankMonitor; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback; private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; @@ -67,17 +70,21 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH public ExitDesktopTaskTransitionHandler( Transitions transitions, - Context context) { - this(transitions, SurfaceControl.Transaction::new, context); + Context context, + InteractionJankMonitor interactionJankMonitor + ) { + this(transitions, SurfaceControl.Transaction::new, context, interactionJankMonitor); } private ExitDesktopTaskTransitionHandler( Transitions transitions, Supplier<SurfaceControl.Transaction> supplier, - Context context) { + Context context, + InteractionJankMonitor interactionJankMonitor) { mTransitions = transitions; mTransactionSupplier = supplier; mContext = context; + mInteractionJankMonitor = interactionJankMonitor; } /** @@ -146,6 +153,8 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH final int screenHeight = metrics.heightPixels; final SurfaceControl sc = change.getLeash(); final Rect endBounds = change.getEndAbsBounds(); + mInteractionJankMonitor + .begin(sc, mContext, Cuj.CUJ_DESKTOP_MODE_EXIT_MODE); // Hide the first (fullscreen) frame because the animation will start from the freeform // size. startT.hide(sc) @@ -175,6 +184,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH if (mOnAnimationFinishedCallback != null) { mOnAnimationFinishedCallback.accept(finishT); } + mInteractionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_EXIT_MODE); mTransitions.getMainExecutor().execute( () -> finishCallback.onTransitionFinished(null)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java index dd4595a70211..3a680097554f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java @@ -27,7 +27,6 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; @@ -76,7 +75,6 @@ public class ShellInit { */ @VisibleForTesting public void init() { - ProtoLog.registerGroups(ShellProtoLogGroup.values()); ProtoLog.v(WM_SHELL_INIT, "Initializing Shell Components: %d", mInitCallbacks.size()); SurfaceControl.setDebugUsageAfterRelease(true); // Init in order of registration diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt index 32217d87b130..c725b08d2f5e 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt @@ -21,11 +21,10 @@ import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd +import android.tools.flicker.assertors.assertions.AppWindowHasSizeOfAtLeast import android.tools.flicker.assertors.assertions.AppWindowIsInvisibleAtEnd -import android.tools.flicker.assertors.assertions.AppWindowIsVisibleAlways import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart -import android.tools.flicker.assertors.assertions.AppWindowRemainInsideDisplayBounds import android.tools.flicker.assertors.assertions.LauncherWindowReplacesAppAsTopWindow import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfigEntry @@ -46,28 +45,27 @@ class DesktopModeFlickerScenarios { FlickerConfigEntry( scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"), extractor = - ShellTransitionScenarioExtractor( - transitionMatcher = - object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - return transitions.filter { - it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP - } - } + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return transitions.filter { + it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP } - ), + } + } + ), assertions = - AssertionTemplates.COMMON_ASSERTIONS + + AssertionTemplates.COMMON_ASSERTIONS + listOf( - AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), - AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), - AppWindowHasDesktopModeInitialBoundsAtTheEnd( - Components.DESKTOP_MODE_APP - ) + AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), + AppWindowHasDesktopModeInitialBoundsAtTheEnd( + Components.DESKTOP_MODE_APP ) - .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) // Use this scenario for closing an app in desktop windowing, except the last app. For the @@ -76,77 +74,85 @@ class DesktopModeFlickerScenarios { FlickerConfigEntry( scenarioId = ScenarioId("CLOSE_APP"), extractor = - ShellTransitionScenarioExtractor( - transitionMatcher = - object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - // In case there are multiple windows closing, filter out the - // last window closing. It should use the CLOSE_LAST_APP - // scenario below. - return transitions - .filter { it.type == TransitionType.CLOSE } - .sortedByDescending { it.id } - .drop(1) - } - } - ), + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + // In case there are multiple windows closing, filter out the + // last window closing. It should use the CLOSE_LAST_APP + // scenario below. + return transitions + .filter { it.type == TransitionType.CLOSE } + .sortedByDescending { it.id } + .drop(1) + } + } + ), assertions = - AssertionTemplates.COMMON_ASSERTIONS + + AssertionTemplates.COMMON_ASSERTIONS + listOf( - AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), - AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), - AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), - ) - .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP), + AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) val CLOSE_LAST_APP = FlickerConfigEntry( scenarioId = ScenarioId("CLOSE_LAST_APP"), extractor = - ShellTransitionScenarioExtractor( - transitionMatcher = - object : ITransitionMatcher { - override fun findAll( - transitions: Collection<Transition> - ): Collection<Transition> { - val lastTransition = - transitions - .filter { it.type == TransitionType.CLOSE } - .maxByOrNull { it.id }!! - return listOf(lastTransition) - } - } - ), + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + val lastTransition = + transitions + .filter { it.type == TransitionType.CLOSE } + .maxByOrNull { it.id }!! + return listOf(lastTransition) + } + } + ), assertions = - AssertionTemplates.COMMON_ASSERTIONS + + AssertionTemplates.COMMON_ASSERTIONS + listOf( - AppWindowIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), - LauncherWindowReplacesAppAsTopWindow(Components.DESKTOP_MODE_APP), - AppWindowIsInvisibleAtEnd(DESKTOP_WALLPAPER) - ) - .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + AppWindowIsInvisibleAtEnd(Components.DESKTOP_MODE_APP), + LauncherWindowReplacesAppAsTopWindow(Components.DESKTOP_MODE_APP), + AppWindowIsInvisibleAtEnd(DESKTOP_WALLPAPER) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) val CORNER_RESIZE = FlickerConfigEntry( scenarioId = ScenarioId("CORNER_RESIZE"), extractor = - TaggedScenarioExtractorBuilder() + TaggedScenarioExtractorBuilder() .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW) .setTransitionMatcher( TaggedCujTransitionMatcher(associatedTransitionRequired = false) ) .build(), + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + ) + + val CORNER_RESIZE_TO_MINIMUM_SIZE = + FlickerConfigEntry( + scenarioId = ScenarioId("CORNER_RESIZE_TO_MINIMUM_SIZE"), + extractor = + TaggedScenarioExtractorBuilder() + .setTargetTag(CujType.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + .setTransitionMatcher( + TaggedCujTransitionMatcher(associatedTransitionRequired = false) + ).build(), assertions = - listOf( - AppWindowIsVisibleAlways(Components.DESKTOP_MODE_APP), - AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP), - AppWindowRemainInsideDisplayBounds(Components.DESKTOP_MODE_APP), - ) - .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + AppWindowHasSizeOfAtLeast(Components.DESKTOP_MODE_APP, 770, 700) + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppToMinimumWindowSizeLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppToMinimumWindowSizeLandscape.kt new file mode 100644 index 000000000000..6319cf74ed8f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppToMinimumWindowSizeLandscape.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 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.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.Rotation +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE_TO_MINIMUM_SIZE +import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Resize app window using corner resize to the smallest possible height and width in + * landscape mode. + * + * Assert that the minimum window size constraint is maintained. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppToMinimumWindowSizeLandscape : ResizeAppWithCornerResize( + rotation = Rotation.ROTATION_90, + horizontalChange = -1500, + verticalChange = 1500) { + @ExpectedScenarios(["CORNER_RESIZE_TO_MINIMUM_SIZE"]) + @Test + override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE_TO_MINIMUM_SIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppToMinimumWindowSizePortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppToMinimumWindowSizePortrait.kt new file mode 100644 index 000000000000..431f6e3d3ea2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppToMinimumWindowSizePortrait.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 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.wm.shell.flicker.service.desktopmode.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE_TO_MINIMUM_SIZE +import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Resize app window using corner resize to the smallest possible height and width in portrait mode. + * + * Assert that the minimum window size constraint is maintained. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class ResizeAppToMinimumWindowSizePortrait : ResizeAppWithCornerResize(horizontalChange = -1500, + verticalChange = 1500) { + @ExpectedScenarios(["CORNER_RESIZE_TO_MINIMUM_SIZE"]) + @Test + override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE_TO_MINIMUM_SIZE) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/MaximizeAppWindow.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/MaximizeAppWindow.kt new file mode 100644 index 000000000000..20e2167c28f2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/MaximizeAppWindow.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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.wm.shell.flicker.service.desktopmode.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.flicker.service.common.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +/** Base scenario test for maximize app window CUJ in desktop mode. */ +@Ignore("Base Test Class") +abstract class MaximizeAppWindow +{ + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, + Rotation.ROTATION_0) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + testApp.enterDesktopWithDrag(wmHelper, device) + } + + @Test + open fun maximizeAppWindow() { + testApp.maximiseDesktopApp(wmHelper, device) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt index ac9089a5c1bd..136cf378aa09 100644 --- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt +++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt @@ -38,7 +38,9 @@ import org.junit.Test @Ignore("Base Test Class") abstract class ResizeAppWithCornerResize @JvmOverloads -constructor(val rotation: Rotation = Rotation.ROTATION_0) { +constructor(val rotation: Rotation = Rotation.ROTATION_0, + val horizontalChange: Int = 50, + val verticalChange: Int = -50) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val tapl = LauncherInstrumentation() @@ -46,7 +48,9 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { private val device = UiDevice.getInstance(instrumentation) private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) - @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) @Before fun setup() { @@ -58,7 +62,11 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { @Test open fun resizeAppWithCornerResize() { - testApp.cornerResize(wmHelper, device, DesktopModeAppHelper.Corners.RIGHT_TOP, 50, -50) + testApp.cornerResize(wmHelper, + device, + DesktopModeAppHelper.Corners.RIGHT_TOP, + horizontalChange, + verticalChange) } @After diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java index b2467e9a62cf..e5157c974e2d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java @@ -45,6 +45,7 @@ import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource; @@ -65,6 +66,8 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { @Mock private Transitions mTransitions; @Mock + private InteractionJankMonitor mInteractionJankMonitor; + @Mock IBinder mToken; @Mock Supplier<SurfaceControl.Transaction> mTransactionFactory; @@ -94,7 +97,7 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { .thenReturn(getContext().getResources().getDisplayMetrics()); mExitDesktopTaskTransitionHandler = new ExitDesktopTaskTransitionHandler(mTransitions, - mContext); + mContext, mInteractionJankMonitor); mPoint = new Point(0, 0); } diff --git a/libs/androidfw/StringPool.cpp b/libs/androidfw/StringPool.cpp index ad445c042e63..629f14683b19 100644 --- a/libs/androidfw/StringPool.cpp +++ b/libs/androidfw/StringPool.cpp @@ -297,24 +297,22 @@ void StringPool::Prune() { template <typename E> static void SortEntries( std::vector<std::unique_ptr<E>>& entries, - const std::function<int(const StringPool::Context&, const StringPool::Context&)>& cmp) { + base::function_ref<int(const StringPool::Context&, const StringPool::Context&)> cmp) { using UEntry = std::unique_ptr<E>; + std::sort(entries.begin(), entries.end(), [cmp](const UEntry& a, const UEntry& b) -> bool { + int r = cmp(a->context, b->context); + if (r == 0) { + r = a->value.compare(b->value); + } + return r < 0; + }); +} - if (cmp != nullptr) { - std::sort(entries.begin(), entries.end(), [&cmp](const UEntry& a, const UEntry& b) -> bool { - int r = cmp(a->context, b->context); - if (r == 0) { - r = a->value.compare(b->value); - } - return r < 0; - }); - } else { - std::sort(entries.begin(), entries.end(), - [](const UEntry& a, const UEntry& b) -> bool { return a->value < b->value; }); - } +void StringPool::Sort() { + Sort([](auto&&, auto&&) { return 0; }); } -void StringPool::Sort(const std::function<int(const Context&, const Context&)>& cmp) { +void StringPool::Sort(base::function_ref<int(const Context&, const Context&)> cmp) { SortEntries(styles_, cmp); SortEntries(strings_, cmp); ReAssignIndices(); diff --git a/libs/androidfw/include/androidfw/StringPool.h b/libs/androidfw/include/androidfw/StringPool.h index 0190ab57bf23..9b2c72a29f48 100644 --- a/libs/androidfw/include/androidfw/StringPool.h +++ b/libs/androidfw/include/androidfw/StringPool.h @@ -17,7 +17,6 @@ #ifndef _ANDROID_STRING_POOL_H #define _ANDROID_STRING_POOL_H -#include <functional> #include <memory> #include <string> #include <unordered_map> @@ -25,6 +24,7 @@ #include "BigBuffer.h" #include "IDiagnostics.h" +#include "android-base/function_ref.h" #include "android-base/macros.h" #include "androidfw/ConfigDescription.h" #include "androidfw/StringPiece.h" @@ -205,7 +205,8 @@ class StringPool { // Sorts the strings according to their Context using some comparison function. // Equal Contexts are further sorted by string value, lexicographically. // If no comparison function is provided, values are only sorted lexicographically. - void Sort(const std::function<int(const Context&, const Context&)>& cmp = nullptr); + void Sort(); + void Sort(base::function_ref<int(const Context&, const Context&)> cmp); // Removes any strings that have no references. void Prune(); diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp index 79c810ca2611..bd84b58aa0f4 100644 --- a/packages/PackageInstaller/Android.bp +++ b/packages/PackageInstaller/Android.bp @@ -46,7 +46,6 @@ android_app { sdk_version: "system_current", rename_resources_package: false, static_libs: [ - "xz-java", "androidx.leanback_leanback", "androidx.annotation_annotation", "androidx.fragment_fragment", @@ -79,7 +78,6 @@ android_app { overrides: ["PackageInstaller"], static_libs: [ - "xz-java", "androidx.leanback_leanback", "androidx.fragment_fragment", "androidx.lifecycle_lifecycle-livedata", @@ -112,7 +110,6 @@ android_app { overrides: ["PackageInstaller"], static_libs: [ - "xz-java", "androidx.leanback_leanback", "androidx.annotation_annotation", "androidx.fragment_fragment", diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index 443747530315..68443a7e1492 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -147,17 +147,6 @@ android:configChanges="mnc|mnc|touchscreen|navigation|screenLayout|screenSize|smallestScreenSize|orientation|locale|keyboard|keyboardHidden|fontScale|uiMode|layoutDirection|density" android:exported="false" /> - <!-- Wearable Components --> - <service android:name=".wear.WearPackageInstallerService" - android:permission="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" - android:foregroundServiceType="systemExempted" - android:exported="true"/> - - <provider android:name=".wear.WearPackageIconProvider" - android:authorities="com.google.android.packageinstaller.wear.provider" - android:grantUriPermissions="true" - android:exported="false" /> - <receiver android:name="androidx.profileinstaller.ProfileInstallReceiver" tools:node="remove" /> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java deleted file mode 100644 index 53a460dc18ca..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallTask.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.packageinstaller.wear; - -import android.content.Context; -import android.content.IntentSender; -import android.content.pm.PackageInstaller; -import android.os.Looper; -import android.os.ParcelFileDescriptor; -import android.text.TextUtils; -import android.util.Log; - -import java.io.Closeable; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Task that installs an APK. This must not be called on the main thread. - * This code is based off the Finsky/Wearsky implementation - */ -public class InstallTask { - private static final String TAG = "InstallTask"; - - private static final int DEFAULT_BUFFER_SIZE = 8192; - - private final Context mContext; - private String mPackageName; - private ParcelFileDescriptor mParcelFileDescriptor; - private PackageInstallerImpl.InstallListener mCallback; - private PackageInstaller.Session mSession; - private IntentSender mCommitCallback; - - private Exception mException = null; - private int mErrorCode = 0; - private String mErrorDesc = null; - - public InstallTask(Context context, String packageName, - ParcelFileDescriptor parcelFileDescriptor, - PackageInstallerImpl.InstallListener callback, PackageInstaller.Session session, - IntentSender commitCallback) { - mContext = context; - mPackageName = packageName; - mParcelFileDescriptor = parcelFileDescriptor; - mCallback = callback; - mSession = session; - mCommitCallback = commitCallback; - } - - public boolean isError() { - return mErrorCode != InstallerConstants.STATUS_SUCCESS || !TextUtils.isEmpty(mErrorDesc); - } - - public void execute() { - if (Looper.myLooper() == Looper.getMainLooper()) { - throw new IllegalStateException("This method cannot be called from the UI thread."); - } - - OutputStream sessionStream = null; - try { - sessionStream = mSession.openWrite(mPackageName, 0, -1); - - // 2b: Stream the asset to the installer. Note: - // Note: writeToOutputStreamFromAsset() always safely closes the input stream - writeToOutputStreamFromAsset(sessionStream); - mSession.fsync(sessionStream); - } catch (Exception e) { - mException = e; - mErrorCode = InstallerConstants.ERROR_INSTALL_COPY_STREAM; - mErrorDesc = "Could not write to stream"; - } finally { - if (sessionStream != null) { - // 2c: close output stream - try { - sessionStream.close(); - } catch (Exception e) { - // Ignore otherwise - if (mException == null) { - mException = e; - mErrorCode = InstallerConstants.ERROR_INSTALL_CLOSE_STREAM; - mErrorDesc = "Could not close session stream"; - } - } - } - } - - if (mErrorCode != InstallerConstants.STATUS_SUCCESS) { - // An error occurred, we're done - Log.e(TAG, "Exception while installing " + mPackageName + ": " + mErrorCode + ", " - + mErrorDesc + ", " + mException); - mSession.close(); - mCallback.installFailed(mErrorCode, "[" + mPackageName + "]" + mErrorDesc); - } else { - // 3. Commit the session (this actually installs it.) Session map - // will be cleaned up in the callback. - mCallback.installBeginning(); - mSession.commit(mCommitCallback); - mSession.close(); - } - } - - /** - * {@code PackageInstaller} works with streams. Get the {@code FileDescriptor} - * corresponding to the {@code Asset} and then write the contents into an - * {@code OutputStream} that is passed in. - * <br> - * The {@code FileDescriptor} is closed but the {@code OutputStream} is not closed. - */ - private boolean writeToOutputStreamFromAsset(OutputStream outputStream) { - if (outputStream == null) { - mErrorCode = InstallerConstants.ERROR_INSTALL_COPY_STREAM_EXCEPTION; - mErrorDesc = "Got a null OutputStream."; - return false; - } - - if (mParcelFileDescriptor == null || mParcelFileDescriptor.getFileDescriptor() == null) { - mErrorCode = InstallerConstants.ERROR_COULD_NOT_GET_FD; - mErrorDesc = "Could not get FD"; - return false; - } - - InputStream inputStream = null; - try { - byte[] inputBuf = new byte[DEFAULT_BUFFER_SIZE]; - int bytesRead; - inputStream = new ParcelFileDescriptor.AutoCloseInputStream(mParcelFileDescriptor); - - while ((bytesRead = inputStream.read(inputBuf)) > -1) { - if (bytesRead > 0) { - outputStream.write(inputBuf, 0, bytesRead); - } - } - - outputStream.flush(); - } catch (IOException e) { - mErrorCode = InstallerConstants.ERROR_INSTALL_APK_COPY_FAILURE; - mErrorDesc = "Reading from Asset FD or writing to temp file failed: " + e; - return false; - } finally { - safeClose(inputStream); - } - - return true; - } - - /** - * Quietly close a closeable resource (e.g. a stream or file). The input may already - * be closed and it may even be null. - */ - public static void safeClose(Closeable resource) { - if (resource != null) { - try { - resource.close(); - } catch (IOException ioe) { - // Catch and discard the error - } - } - } -}
\ No newline at end of file diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java deleted file mode 100644 index 3daf3d831d97..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/InstallerConstants.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.packageinstaller.wear; - -/** - * Constants for Installation / Uninstallation requests. - * Using the same values as Finsky/Wearsky code for consistency in user analytics of failures - */ -public class InstallerConstants { - /** Request succeeded */ - public static final int STATUS_SUCCESS = 0; - - /** - * The new PackageInstaller also returns a small set of less granular error codes, which - * we'll remap to the range -500 and below to keep away from existing installer codes - * (which run from -1 to -110). - */ - public final static int ERROR_PACKAGEINSTALLER_BASE = -500; - - public static final int ERROR_COULD_NOT_GET_FD = -603; - /** This node is not targeted by this request. */ - - /** The install did not complete because could not create PackageInstaller session */ - public final static int ERROR_INSTALL_CREATE_SESSION = -612; - /** The install did not complete because could not open PackageInstaller session */ - public final static int ERROR_INSTALL_OPEN_SESSION = -613; - /** The install did not complete because could not open PackageInstaller output stream */ - public final static int ERROR_INSTALL_OPEN_STREAM = -614; - /** The install did not complete because of an exception while streaming bytes */ - public final static int ERROR_INSTALL_COPY_STREAM_EXCEPTION = -615; - /** The install did not complete because of an unexpected exception from PackageInstaller */ - public final static int ERROR_INSTALL_SESSION_EXCEPTION = -616; - /** The install did not complete because of an unexpected userActionRequired callback */ - public final static int ERROR_INSTALL_USER_ACTION_REQUIRED = -617; - /** The install did not complete because of an unexpected broadcast (missing fields) */ - public final static int ERROR_INSTALL_MALFORMED_BROADCAST = -618; - /** The install did not complete because of an error while copying from downloaded file */ - public final static int ERROR_INSTALL_APK_COPY_FAILURE = -619; - /** The install did not complete because of an error while copying to the PackageInstaller - * output stream */ - public final static int ERROR_INSTALL_COPY_STREAM = -620; - /** The install did not complete because of an error while closing the PackageInstaller - * output stream */ - public final static int ERROR_INSTALL_CLOSE_STREAM = -621; -}
\ No newline at end of file diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java deleted file mode 100644 index bdc22cf0e276..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerFactory.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.packageinstaller.wear; - -import android.content.Context; - -/** - * Factory that creates a Package Installer. - */ -public class PackageInstallerFactory { - private static PackageInstallerImpl sPackageInstaller; - - /** - * Return the PackageInstaller shared object. {@code init} should have already been called. - */ - public synchronized static PackageInstallerImpl getPackageInstaller(Context context) { - if (sPackageInstaller == null) { - sPackageInstaller = new PackageInstallerImpl(context); - } - return sPackageInstaller; - } -}
\ No newline at end of file diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java deleted file mode 100644 index 1e37f15f714d..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java +++ /dev/null @@ -1,327 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.packageinstaller.wear; - -import android.annotation.TargetApi; -import android.app.PendingIntent; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.IntentSender; -import android.content.pm.PackageInstaller; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Implementation of package manager installation using modern PackageInstaller api. - * - * Heavily copied from Wearsky/Finsky implementation - */ -@TargetApi(Build.VERSION_CODES.LOLLIPOP) -public class PackageInstallerImpl { - private static final String TAG = "PackageInstallerImpl"; - - /** Intent actions used for broadcasts from PackageInstaller back to the local receiver */ - private static final String ACTION_INSTALL_COMMIT = - "com.android.vending.INTENT_PACKAGE_INSTALL_COMMIT"; - - private final Context mContext; - private final PackageInstaller mPackageInstaller; - private final Map<String, PackageInstaller.SessionInfo> mSessionInfoMap; - private final Map<String, PackageInstaller.Session> mOpenSessionMap; - - public PackageInstallerImpl(Context context) { - mContext = context.getApplicationContext(); - mPackageInstaller = mContext.getPackageManager().getPackageInstaller(); - - // Capture a map of known sessions - // This list will be pruned a bit later (stale sessions will be canceled) - mSessionInfoMap = new HashMap<String, PackageInstaller.SessionInfo>(); - List<PackageInstaller.SessionInfo> mySessions = mPackageInstaller.getMySessions(); - for (int i = 0; i < mySessions.size(); i++) { - PackageInstaller.SessionInfo sessionInfo = mySessions.get(i); - String packageName = sessionInfo.getAppPackageName(); - PackageInstaller.SessionInfo oldInfo = mSessionInfoMap.put(packageName, sessionInfo); - - // Checking for old info is strictly for logging purposes - if (oldInfo != null) { - Log.w(TAG, "Multiple sessions for " + packageName + " found. Removing " + oldInfo - .getSessionId() + " & keeping " + mySessions.get(i).getSessionId()); - } - } - mOpenSessionMap = new HashMap<String, PackageInstaller.Session>(); - } - - /** - * This callback will be made after an installation attempt succeeds or fails. - */ - public interface InstallListener { - /** - * This callback signals that preflight checks have succeeded and installation - * is beginning. - */ - void installBeginning(); - - /** - * This callback signals that installation has completed. - */ - void installSucceeded(); - - /** - * This callback signals that installation has failed. - */ - void installFailed(int errorCode, String errorDesc); - } - - /** - * This is a placeholder implementation that bundles an entire "session" into a single - * call. This will be replaced by more granular versions that allow longer session lifetimes, - * download progress tracking, etc. - * - * This must not be called on main thread. - */ - public void install(final String packageName, ParcelFileDescriptor parcelFileDescriptor, - final InstallListener callback) { - // 0. Generic try/catch block because I am not really sure what exceptions (other than - // IOException) might be thrown by PackageInstaller and I want to handle them - // at least slightly gracefully. - try { - // 1. Create or recover a session, and open it - // Try recovery first - PackageInstaller.Session session = null; - PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName); - if (sessionInfo != null) { - // See if it's openable, or already held open - session = getSession(packageName); - } - // If open failed, or there was no session, create a new one and open it. - // If we cannot create or open here, the failure is terminal. - if (session == null) { - try { - innerCreateSession(packageName); - } catch (IOException ioe) { - Log.e(TAG, "Can't create session for " + packageName + ": " + ioe.getMessage()); - callback.installFailed(InstallerConstants.ERROR_INSTALL_CREATE_SESSION, - "Could not create session"); - mSessionInfoMap.remove(packageName); - return; - } - sessionInfo = mSessionInfoMap.get(packageName); - try { - session = mPackageInstaller.openSession(sessionInfo.getSessionId()); - mOpenSessionMap.put(packageName, session); - } catch (SecurityException se) { - Log.e(TAG, "Can't open session for " + packageName + ": " + se.getMessage()); - callback.installFailed(InstallerConstants.ERROR_INSTALL_OPEN_SESSION, - "Can't open session"); - mSessionInfoMap.remove(packageName); - return; - } - } - - // 2. Launch task to handle file operations. - InstallTask task = new InstallTask( mContext, packageName, parcelFileDescriptor, - callback, session, - getCommitCallback(packageName, sessionInfo.getSessionId(), callback)); - task.execute(); - if (task.isError()) { - cancelSession(sessionInfo.getSessionId(), packageName); - } - } catch (Exception e) { - Log.e(TAG, "Unexpected exception while installing: " + packageName + ": " - + e.getMessage()); - callback.installFailed(InstallerConstants.ERROR_INSTALL_SESSION_EXCEPTION, - "Unexpected exception while installing " + packageName); - } - } - - /** - * Retrieve an existing session. Will open if needed, but does not attempt to create. - */ - private PackageInstaller.Session getSession(String packageName) { - // Check for already-open session - PackageInstaller.Session session = mOpenSessionMap.get(packageName); - if (session != null) { - try { - // Probe the session to ensure that it's still open. This may or may not - // throw (if non-open), but it may serve as a canary for stale sessions. - session.getNames(); - return session; - } catch (IOException ioe) { - Log.e(TAG, "Stale open session for " + packageName + ": " + ioe.getMessage()); - mOpenSessionMap.remove(packageName); - } catch (SecurityException se) { - Log.e(TAG, "Stale open session for " + packageName + ": " + se.getMessage()); - mOpenSessionMap.remove(packageName); - } - } - // Check to see if this is a known session - PackageInstaller.SessionInfo sessionInfo = mSessionInfoMap.get(packageName); - if (sessionInfo == null) { - return null; - } - // Try to open it. If we fail here, assume that the SessionInfo was stale. - try { - session = mPackageInstaller.openSession(sessionInfo.getSessionId()); - } catch (SecurityException se) { - Log.w(TAG, "SessionInfo was stale for " + packageName + " - deleting info"); - mSessionInfoMap.remove(packageName); - return null; - } catch (IOException ioe) { - Log.w(TAG, "IOException opening old session for " + ioe.getMessage() - + " - deleting info"); - mSessionInfoMap.remove(packageName); - return null; - } - mOpenSessionMap.put(packageName, session); - return session; - } - - /** This version throws an IOException when the session cannot be created */ - private void innerCreateSession(String packageName) throws IOException { - if (mSessionInfoMap.containsKey(packageName)) { - Log.w(TAG, "Creating session for " + packageName + " when one already exists"); - return; - } - PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( - PackageInstaller.SessionParams.MODE_FULL_INSTALL); - params.setAppPackageName(packageName); - - // IOException may be thrown at this point - int sessionId = mPackageInstaller.createSession(params); - PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); - mSessionInfoMap.put(packageName, sessionInfo); - } - - /** - * Cancel a session based on its sessionId. Package name is for logging only. - */ - private void cancelSession(int sessionId, String packageName) { - // Close if currently held open - closeSession(packageName); - // Remove local record - mSessionInfoMap.remove(packageName); - try { - mPackageInstaller.abandonSession(sessionId); - } catch (SecurityException se) { - // The session no longer exists, so we can exit quietly. - return; - } - } - - /** - * Close a session if it happens to be held open. - */ - private void closeSession(String packageName) { - PackageInstaller.Session session = mOpenSessionMap.remove(packageName); - if (session != null) { - // Unfortunately close() is not idempotent. Try our best to make this safe. - try { - session.close(); - } catch (Exception e) { - Log.w(TAG, "Unexpected error closing session for " + packageName + ": " - + e.getMessage()); - } - } - } - - /** - * Creates a commit callback for the package install that's underway. This will be called - * some time after calling session.commit() (above). - */ - private IntentSender getCommitCallback(final String packageName, final int sessionId, - final InstallListener callback) { - // Create a single-use broadcast receiver - BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - mContext.unregisterReceiver(this); - handleCommitCallback(intent, packageName, sessionId, callback); - } - }; - // Create a matching intent-filter and register the receiver - String action = ACTION_INSTALL_COMMIT + "." + packageName; - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(action); - mContext.registerReceiver(broadcastReceiver, intentFilter, - Context.RECEIVER_EXPORTED); - - // Create a matching PendingIntent and use it to generate the IntentSender - Intent broadcastIntent = new Intent(action).setPackage(mContext.getPackageName()); - PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, packageName.hashCode(), - broadcastIntent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT - | PendingIntent.FLAG_MUTABLE); - return pendingIntent.getIntentSender(); - } - - /** - * Examine the extras to determine information about the package update/install, decode - * the result, and call the appropriate callback. - * - * @param intent The intent, which the PackageInstaller will have added Extras to - * @param packageName The package name we created the receiver for - * @param sessionId The session Id we created the receiver for - * @param callback The callback to report success/failure to - */ - private void handleCommitCallback(Intent intent, String packageName, int sessionId, - InstallListener callback) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Installation of " + packageName + " finished with extras " - + intent.getExtras()); - } - String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); - int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE); - if (status == PackageInstaller.STATUS_SUCCESS) { - cancelSession(sessionId, packageName); - callback.installSucceeded(); - } else if (status == -1 /*PackageInstaller.STATUS_USER_ACTION_REQUIRED*/) { - // TODO - use the constant when the correct/final name is in the SDK - // TODO This is unexpected, so we are treating as failure for now - cancelSession(sessionId, packageName); - callback.installFailed(InstallerConstants.ERROR_INSTALL_USER_ACTION_REQUIRED, - "Unexpected: user action required"); - } else { - cancelSession(sessionId, packageName); - int errorCode = getPackageManagerErrorCode(status); - Log.e(TAG, "Error " + errorCode + " while installing " + packageName + ": " - + statusMessage); - callback.installFailed(errorCode, null); - } - } - - private int getPackageManagerErrorCode(int status) { - // This is a hack: because PackageInstaller now reports error codes - // with small positive values, we need to remap them into a space - // that is more compatible with the existing package manager error codes. - // See https://sites.google.com/a/google.com/universal-store/documentation - // /android-client/download-error-codes - int errorCode; - if (status == Integer.MIN_VALUE) { - errorCode = InstallerConstants.ERROR_INSTALL_MALFORMED_BROADCAST; - } else { - errorCode = InstallerConstants.ERROR_PACKAGEINSTALLER_BASE - status; - } - return errorCode; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java deleted file mode 100644 index 2c289b2a6f94..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageArgs.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.packageinstaller.wear; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; - -/** - * Installation Util that contains a list of parameters that are needed for - * installing/uninstalling. - */ -public class WearPackageArgs { - private static final String KEY_PACKAGE_NAME = - "com.google.android.clockwork.EXTRA_PACKAGE_NAME"; - private static final String KEY_ASSET_URI = - "com.google.android.clockwork.EXTRA_ASSET_URI"; - private static final String KEY_START_ID = - "com.google.android.clockwork.EXTRA_START_ID"; - private static final String KEY_PERM_URI = - "com.google.android.clockwork.EXTRA_PERM_URI"; - private static final String KEY_CHECK_PERMS = - "com.google.android.clockwork.EXTRA_CHECK_PERMS"; - private static final String KEY_SKIP_IF_SAME_VERSION = - "com.google.android.clockwork.EXTRA_SKIP_IF_SAME_VERSION"; - private static final String KEY_COMPRESSION_ALG = - "com.google.android.clockwork.EXTRA_KEY_COMPRESSION_ALG"; - private static final String KEY_COMPANION_SDK_VERSION = - "com.google.android.clockwork.EXTRA_KEY_COMPANION_SDK_VERSION"; - private static final String KEY_COMPANION_DEVICE_VERSION = - "com.google.android.clockwork.EXTRA_KEY_COMPANION_DEVICE_VERSION"; - private static final String KEY_SHOULD_CHECK_GMS_DEPENDENCY = - "com.google.android.clockwork.EXTRA_KEY_SHOULD_CHECK_GMS_DEPENDENCY"; - private static final String KEY_SKIP_IF_LOWER_VERSION = - "com.google.android.clockwork.EXTRA_SKIP_IF_LOWER_VERSION"; - - public static String getPackageName(Bundle b) { - return b.getString(KEY_PACKAGE_NAME); - } - - public static Bundle setPackageName(Bundle b, String packageName) { - b.putString(KEY_PACKAGE_NAME, packageName); - return b; - } - - public static Uri getAssetUri(Bundle b) { - return b.getParcelable(KEY_ASSET_URI); - } - - public static Uri getPermUri(Bundle b) { - return b.getParcelable(KEY_PERM_URI); - } - - public static boolean checkPerms(Bundle b) { - return b.getBoolean(KEY_CHECK_PERMS); - } - - public static boolean skipIfSameVersion(Bundle b) { - return b.getBoolean(KEY_SKIP_IF_SAME_VERSION); - } - - public static int getCompanionSdkVersion(Bundle b) { - return b.getInt(KEY_COMPANION_SDK_VERSION); - } - - public static int getCompanionDeviceVersion(Bundle b) { - return b.getInt(KEY_COMPANION_DEVICE_VERSION); - } - - public static String getCompressionAlg(Bundle b) { - return b.getString(KEY_COMPRESSION_ALG); - } - - public static int getStartId(Bundle b) { - return b.getInt(KEY_START_ID); - } - - public static boolean skipIfLowerVersion(Bundle b) { - return b.getBoolean(KEY_SKIP_IF_LOWER_VERSION, false); - } - - public static Bundle setStartId(Bundle b, int startId) { - b.putInt(KEY_START_ID, startId); - return b; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java deleted file mode 100644 index 02b9d298db0e..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageIconProvider.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.packageinstaller.wear; - -import android.annotation.TargetApi; -import android.app.ActivityManager; -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.database.Cursor; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.List; - -import static android.content.pm.PackageManager.PERMISSION_GRANTED; - -public class WearPackageIconProvider extends ContentProvider { - private static final String TAG = "WearPackageIconProvider"; - public static final String AUTHORITY = "com.google.android.packageinstaller.wear.provider"; - - private static final String REQUIRED_PERMISSION = - "com.google.android.permission.INSTALL_WEARABLE_PACKAGES"; - - /** MIME types. */ - public static final String ICON_TYPE = "vnd.android.cursor.item/cw_package_icon"; - - @Override - public boolean onCreate() { - return true; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - throw new UnsupportedOperationException("Query is not supported."); - } - - @Override - public String getType(Uri uri) { - if (uri == null) { - throw new IllegalArgumentException("URI passed in is null."); - } - - if (AUTHORITY.equals(uri.getEncodedAuthority())) { - return ICON_TYPE; - } - return null; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - throw new UnsupportedOperationException("Insert is not supported."); - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - if (uri == null) { - throw new IllegalArgumentException("URI passed in is null."); - } - - enforcePermissions(uri); - - if (ICON_TYPE.equals(getType(uri))) { - final File file = WearPackageUtil.getIconFile( - this.getContext().getApplicationContext(), getPackageNameFromUri(uri)); - if (file != null) { - file.delete(); - } - } - - return 0; - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException("Update is not supported."); - } - - @Override - public ParcelFileDescriptor openFile( - Uri uri, @SuppressWarnings("unused") String mode) throws FileNotFoundException { - if (uri == null) { - throw new IllegalArgumentException("URI passed in is null."); - } - - enforcePermissions(uri); - - if (ICON_TYPE.equals(getType(uri))) { - final File file = WearPackageUtil.getIconFile( - this.getContext().getApplicationContext(), getPackageNameFromUri(uri)); - if (file != null) { - return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); - } - } - return null; - } - - public static Uri getUriForPackage(final String packageName) { - return Uri.parse("content://" + AUTHORITY + "/icons/" + packageName + ".icon"); - } - - private String getPackageNameFromUri(Uri uri) { - if (uri == null) { - return null; - } - List<String> pathSegments = uri.getPathSegments(); - String packageName = pathSegments.get(pathSegments.size() - 1); - - if (packageName.endsWith(".icon")) { - packageName = packageName.substring(0, packageName.lastIndexOf(".")); - } - return packageName; - } - - /** - * Make sure the calling app is either a system app or the same app or has the right permission. - * @throws SecurityException if the caller has insufficient permissions. - */ - @TargetApi(Build.VERSION_CODES.BASE_1_1) - private void enforcePermissions(Uri uri) { - // Redo some of the permission check in {@link ContentProvider}. Just add an extra check to - // allow System process to access this provider. - Context context = getContext(); - final int pid = Binder.getCallingPid(); - final int uid = Binder.getCallingUid(); - final int myUid = android.os.Process.myUid(); - - if (uid == myUid || isSystemApp(context, pid)) { - return; - } - - if (context.checkPermission(REQUIRED_PERMISSION, pid, uid) == PERMISSION_GRANTED) { - return; - } - - // last chance, check against any uri grants - if (context.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION) - == PERMISSION_GRANTED) { - return; - } - - throw new SecurityException("Permission Denial: reading " - + getClass().getName() + " uri " + uri + " from pid=" + pid - + ", uid=" + uid); - } - - /** - * From the pid of the calling process, figure out whether this is a system app or not. We do - * this by checking the application information corresponding to the pid and then checking if - * FLAG_SYSTEM is set. - */ - @TargetApi(Build.VERSION_CODES.CUPCAKE) - private boolean isSystemApp(Context context, int pid) { - // Get the Activity Manager Object - ActivityManager aManager = - (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - // Get the list of running Applications - List<ActivityManager.RunningAppProcessInfo> rapInfoList = - aManager.getRunningAppProcesses(); - for (ActivityManager.RunningAppProcessInfo rapInfo : rapInfoList) { - if (rapInfo.pid == pid) { - try { - PackageInfo pkgInfo = context.getPackageManager().getPackageInfo( - rapInfo.pkgList[0], 0); - if (pkgInfo != null && pkgInfo.applicationInfo != null && - (pkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { - Log.d(TAG, pid + " is a system app."); - return true; - } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "Could not find package information.", e); - return false; - } - } - } - return false; - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java deleted file mode 100644 index ae0f4ece1c17..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java +++ /dev/null @@ -1,621 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.packageinstaller.wear; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.FeatureInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageInstaller; -import android.content.pm.PackageManager; -import android.content.pm.VersionedPackage; -import android.database.Cursor; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Looper; -import android.os.Message; -import android.os.ParcelFileDescriptor; -import android.os.PowerManager; -import android.os.Process; -import android.util.ArrayMap; -import android.util.Log; -import android.util.Pair; -import androidx.annotation.Nullable; -import com.android.packageinstaller.DeviceUtils; -import com.android.packageinstaller.PackageUtil; -import com.android.packageinstaller.R; -import com.android.packageinstaller.common.EventResultPersister; -import com.android.packageinstaller.common.UninstallEventReceiver; -import java.io.File; -import java.io.FileNotFoundException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Service that will install/uninstall packages. It will check for permissions and features as well. - * - * ----------- - * - * Debugging information: - * - * Install Action example: - * adb shell am startservice -a com.android.packageinstaller.wear.INSTALL_PACKAGE \ - * -d package://com.google.android.gms \ - * --eu com.google.android.clockwork.EXTRA_ASSET_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/wearable/com.google.android.gms/apk \ - * --es android.intent.extra.INSTALLER_PACKAGE_NAME com.google.android.gms \ - * --ez com.google.android.clockwork.EXTRA_CHECK_PERMS false \ - * --eu com.google.android.clockwork.EXTRA_PERM_URI content://com.google.android.clockwork.home.provider/host/com.google.android.wearable.app/permissions \ - * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService - * - * Uninstall Action example: - * adb shell am startservice -a com.android.packageinstaller.wear.UNINSTALL_PACKAGE \ - * -d package://com.google.android.gms \ - * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService - * - * Retry GMS: - * adb shell am startservice -a com.android.packageinstaller.wear.RETRY_GMS \ - * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService - */ -public class WearPackageInstallerService extends Service - implements EventResultPersister.EventResultObserver { - private static final String TAG = "WearPkgInstallerService"; - - private static final String WEAR_APPS_CHANNEL = "wear_app_install_uninstall"; - private static final String BROADCAST_ACTION = - "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT"; - - private final int START_INSTALL = 1; - private final int START_UNINSTALL = 2; - - private int mInstallNotificationId = 1; - private final Map<String, Integer> mNotifIdMap = new ArrayMap<>(); - private final Map<Integer, UninstallParams> mServiceIdToParams = new HashMap<>(); - - private class UninstallParams { - public String mPackageName; - public PowerManager.WakeLock mLock; - - UninstallParams(String packageName, PowerManager.WakeLock lock) { - mPackageName = packageName; - mLock = lock; - } - } - - private final class ServiceHandler extends Handler { - public ServiceHandler(Looper looper) { - super(looper); - } - - public void handleMessage(Message msg) { - switch (msg.what) { - case START_INSTALL: - installPackage(msg.getData()); - break; - case START_UNINSTALL: - uninstallPackage(msg.getData()); - break; - } - } - } - private ServiceHandler mServiceHandler; - private NotificationChannel mNotificationChannel; - private static volatile PowerManager.WakeLock lockStatic = null; - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onCreate() { - super.onCreate(); - HandlerThread thread = new HandlerThread("PackageInstallerThread", - Process.THREAD_PRIORITY_BACKGROUND); - thread.start(); - - mServiceHandler = new ServiceHandler(thread.getLooper()); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (!DeviceUtils.isWear(this)) { - Log.w(TAG, "Not running on wearable."); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - - if (intent == null) { - Log.w(TAG, "Got null intent."); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Got install/uninstall request " + intent); - } - - Uri packageUri = intent.getData(); - if (packageUri == null) { - Log.e(TAG, "No package URI in intent"); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - - final String packageName = WearPackageUtil.getSanitizedPackageName(packageUri); - if (packageName == null) { - Log.e(TAG, "Invalid package name in URI (expected package:<pkgName>): " + packageUri); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - - PowerManager.WakeLock lock = getLock(this.getApplicationContext()); - if (!lock.isHeld()) { - lock.acquire(); - } - - Bundle intentBundle = intent.getExtras(); - if (intentBundle == null) { - intentBundle = new Bundle(); - } - WearPackageArgs.setStartId(intentBundle, startId); - WearPackageArgs.setPackageName(intentBundle, packageName); - Message msg; - String notifTitle; - if (Intent.ACTION_INSTALL_PACKAGE.equals(intent.getAction())) { - msg = mServiceHandler.obtainMessage(START_INSTALL); - notifTitle = getString(R.string.installing); - } else if (Intent.ACTION_UNINSTALL_PACKAGE.equals(intent.getAction())) { - msg = mServiceHandler.obtainMessage(START_UNINSTALL); - notifTitle = getString(R.string.uninstalling); - } else { - Log.e(TAG, "Unknown action : " + intent.getAction()); - finishServiceEarly(startId); - return START_NOT_STICKY; - } - Pair<Integer, Notification> notifPair = buildNotification(packageName, notifTitle); - startForeground(notifPair.first, notifPair.second); - msg.setData(intentBundle); - mServiceHandler.sendMessage(msg); - return START_NOT_STICKY; - } - - private void installPackage(Bundle argsBundle) { - int startId = WearPackageArgs.getStartId(argsBundle); - final String packageName = WearPackageArgs.getPackageName(argsBundle); - final Uri assetUri = WearPackageArgs.getAssetUri(argsBundle); - final Uri permUri = WearPackageArgs.getPermUri(argsBundle); - boolean checkPerms = WearPackageArgs.checkPerms(argsBundle); - boolean skipIfSameVersion = WearPackageArgs.skipIfSameVersion(argsBundle); - int companionSdkVersion = WearPackageArgs.getCompanionSdkVersion(argsBundle); - int companionDeviceVersion = WearPackageArgs.getCompanionDeviceVersion(argsBundle); - String compressionAlg = WearPackageArgs.getCompressionAlg(argsBundle); - boolean skipIfLowerVersion = WearPackageArgs.skipIfLowerVersion(argsBundle); - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Installing package: " + packageName + ", assetUri: " + assetUri + - ",permUri: " + permUri + ", startId: " + startId + ", checkPerms: " + - checkPerms + ", skipIfSameVersion: " + skipIfSameVersion + - ", compressionAlg: " + compressionAlg + ", companionSdkVersion: " + - companionSdkVersion + ", companionDeviceVersion: " + companionDeviceVersion + - ", skipIfLowerVersion: " + skipIfLowerVersion); - } - final PackageManager pm = getPackageManager(); - File tempFile = null; - PowerManager.WakeLock lock = getLock(this.getApplicationContext()); - boolean messageSent = false; - try { - PackageInfo existingPkgInfo = null; - try { - existingPkgInfo = pm.getPackageInfo(packageName, - PackageManager.MATCH_ANY_USER | PackageManager.GET_PERMISSIONS); - if (existingPkgInfo != null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Replacing package:" + packageName); - } - } - } catch (PackageManager.NameNotFoundException e) { - // Ignore this exception. We could not find the package, will treat as a new - // installation. - } - // TODO(28021618): This was left as a temp file due to the fact that this code is being - // deprecated and that we need the bare minimum to continue working moving forward - // If this code is used as reference, this permission logic might want to be - // reworked to use a stream instead of a file so that we don't need to write a - // file at all. Note that there might be some trickiness with opening a stream - // for multiple users. - ParcelFileDescriptor parcelFd = getContentResolver() - .openFileDescriptor(assetUri, "r"); - tempFile = WearPackageUtil.getFileFromFd(WearPackageInstallerService.this, - parcelFd, packageName, compressionAlg); - if (tempFile == null) { - Log.e(TAG, "Could not create a temp file from FD for " + packageName); - return; - } - PackageInfo pkgInfo = PackageUtil.getPackageInfo(this, tempFile, - PackageManager.GET_PERMISSIONS | PackageManager.GET_CONFIGURATIONS); - if (pkgInfo == null) { - Log.e(TAG, "Could not parse apk information for " + packageName); - return; - } - - if (!pkgInfo.packageName.equals(packageName)) { - Log.e(TAG, "Wearable Package Name has to match what is provided for " + - packageName); - return; - } - - ApplicationInfo appInfo = pkgInfo.applicationInfo; - appInfo.sourceDir = tempFile.getPath(); - appInfo.publicSourceDir = tempFile.getPath(); - getLabelAndUpdateNotification(packageName, - getString(R.string.installing_app, appInfo.loadLabel(pm))); - - List<String> wearablePerms = Arrays.asList(pkgInfo.requestedPermissions); - - // Log if the installed pkg has a higher version number. - if (existingPkgInfo != null) { - long longVersionCode = pkgInfo.getLongVersionCode(); - if (existingPkgInfo.getLongVersionCode() == longVersionCode) { - if (skipIfSameVersion) { - Log.w(TAG, "Version number (" + longVersionCode + - ") of new app is equal to existing app for " + packageName + - "; not installing due to versionCheck"); - return; - } else { - Log.w(TAG, "Version number of new app (" + longVersionCode + - ") is equal to existing app for " + packageName); - } - } else if (existingPkgInfo.getLongVersionCode() > longVersionCode) { - if (skipIfLowerVersion) { - // Starting in Feldspar, we are not going to allow downgrades of any app. - Log.w(TAG, "Version number of new app (" + longVersionCode + - ") is lower than existing app ( " - + existingPkgInfo.getLongVersionCode() + - ") for " + packageName + "; not installing due to versionCheck"); - return; - } else { - Log.w(TAG, "Version number of new app (" + longVersionCode + - ") is lower than existing app ( " - + existingPkgInfo.getLongVersionCode() + ") for " + packageName); - } - } - - // Following the Android Phone model, we should only check for permissions for any - // newly defined perms. - if (existingPkgInfo.requestedPermissions != null) { - for (int i = 0; i < existingPkgInfo.requestedPermissions.length; ++i) { - // If the permission is granted, then we will not ask to request it again. - if ((existingPkgInfo.requestedPermissionsFlags[i] & - PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, existingPkgInfo.requestedPermissions[i] + - " is already granted for " + packageName); - } - wearablePerms.remove(existingPkgInfo.requestedPermissions[i]); - } - } - } - } - - // Check that the wearable has all the features. - boolean hasAllFeatures = true; - for (FeatureInfo feature : pkgInfo.reqFeatures) { - if (feature.name != null && !pm.hasSystemFeature(feature.name) && - (feature.flags & FeatureInfo.FLAG_REQUIRED) != 0) { - Log.e(TAG, "Wearable does not have required feature: " + feature + - " for " + packageName); - hasAllFeatures = false; - } - } - - if (!hasAllFeatures) { - return; - } - - // Check permissions on both the new wearable package and also on the already installed - // wearable package. - // If the app is targeting API level 23, we will also start a service in ClockworkHome - // which will ultimately prompt the user to accept/reject permissions. - if (checkPerms && !checkPermissions(pkgInfo, companionSdkVersion, - companionDeviceVersion, permUri, wearablePerms, tempFile)) { - Log.w(TAG, "Wearable does not have enough permissions."); - return; - } - - // Finally install the package. - ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(assetUri, "r"); - PackageInstallerFactory.getPackageInstaller(this).install(packageName, fd, - new PackageInstallListener(this, lock, startId, packageName)); - - messageSent = true; - Log.i(TAG, "Sent installation request for " + packageName); - } catch (FileNotFoundException e) { - Log.e(TAG, "Could not find the file with URI " + assetUri, e); - } finally { - if (!messageSent) { - // Some error happened. If the message has been sent, we can wait for the observer - // which will finish the service. - if (tempFile != null) { - tempFile.delete(); - } - finishService(lock, startId); - } - } - } - - // TODO: This was left using the old PackageManager API due to the fact that this code is being - // deprecated and that we need the bare minimum to continue working moving forward - // If this code is used as reference, this logic should be reworked to use the new - // PackageInstaller APIs similar to how installPackage was reworked - private void uninstallPackage(Bundle argsBundle) { - int startId = WearPackageArgs.getStartId(argsBundle); - final String packageName = WearPackageArgs.getPackageName(argsBundle); - - PowerManager.WakeLock lock = getLock(this.getApplicationContext()); - - UninstallParams params = new UninstallParams(packageName, lock); - mServiceIdToParams.put(startId, params); - - final PackageManager pm = getPackageManager(); - try { - PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0); - getLabelAndUpdateNotification(packageName, - getString(R.string.uninstalling_app, pkgInfo.applicationInfo.loadLabel(pm))); - - int uninstallId = UninstallEventReceiver.addObserver(this, - EventResultPersister.GENERATE_NEW_ID, this); - - Intent broadcastIntent = new Intent(BROADCAST_ACTION); - broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); - broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId); - broadcastIntent.putExtra(EventResultPersister.EXTRA_SERVICE_ID, startId); - broadcastIntent.setPackage(getPackageName()); - - PendingIntent pendingIntent = PendingIntent.getBroadcast(this, uninstallId, - broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT - | PendingIntent.FLAG_MUTABLE); - - // Found package, send uninstall request. - pm.getPackageInstaller().uninstall( - new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), - PackageManager.DELETE_ALL_USERS, - pendingIntent.getIntentSender()); - - Log.i(TAG, "Sent delete request for " + packageName); - } catch (IllegalArgumentException | PackageManager.NameNotFoundException e) { - // Couldn't find the package, no need to call uninstall. - Log.w(TAG, "Could not find package, not deleting " + packageName, e); - finishService(lock, startId); - } catch (EventResultPersister.OutOfIdsException e) { - Log.e(TAG, "Fails to start uninstall", e); - finishService(lock, startId); - } - } - - @Override - public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) { - if (mServiceIdToParams.containsKey(serviceId)) { - UninstallParams params = mServiceIdToParams.get(serviceId); - try { - if (status == PackageInstaller.STATUS_SUCCESS) { - Log.i(TAG, "Package " + params.mPackageName + " was uninstalled."); - } else { - Log.e(TAG, "Package uninstall failed " + params.mPackageName - + ", returnCode " + legacyStatus); - } - } finally { - finishService(params.mLock, serviceId); - } - } - } - - private boolean checkPermissions(PackageInfo pkgInfo, int companionSdkVersion, - int companionDeviceVersion, Uri permUri, List<String> wearablePermissions, - File apkFile) { - // Assumption: We are running on Android O. - // If the Phone App is targeting M, all permissions may not have been granted to the phone - // app. If the Wear App is then not targeting M, there may be permissions that are not - // granted on the Phone app (by the user) right now and we cannot just grant it for the Wear - // app. - if (pkgInfo.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M) { - // Install the app if Wear App is ready for the new perms model. - return true; - } - - if (!doesWearHaveUngrantedPerms(pkgInfo.packageName, permUri, wearablePermissions)) { - // All permissions requested by the watch are already granted on the phone, no need - // to do anything. - return true; - } - - // Log an error if Wear is targeting < 23 and phone is targeting >= 23. - if (companionSdkVersion == 0 || companionSdkVersion >= Build.VERSION_CODES.M) { - Log.e(TAG, "MNC: Wear app's targetSdkVersion should be at least 23, if " - + "phone app is targeting at least 23, will continue."); - } - - return false; - } - - /** - * Given a {@string packageName} corresponding to a phone app, query the provider for all the - * perms that are granted. - * - * @return true if the Wear App has any perms that have not been granted yet on the phone side. - * @return true if there is any error cases. - */ - private boolean doesWearHaveUngrantedPerms(String packageName, Uri permUri, - List<String> wearablePermissions) { - if (permUri == null) { - Log.e(TAG, "Permission URI is null"); - // Pretend there is an ungranted permission to avoid installing for error cases. - return true; - } - Cursor permCursor = getContentResolver().query(permUri, null, null, null, null); - if (permCursor == null) { - Log.e(TAG, "Could not get the cursor for the permissions"); - // Pretend there is an ungranted permission to avoid installing for error cases. - return true; - } - - Set<String> grantedPerms = new HashSet<>(); - Set<String> ungrantedPerms = new HashSet<>(); - while(permCursor.moveToNext()) { - // Make sure that the MatrixCursor returned by the ContentProvider has 2 columns and - // verify their types. - if (permCursor.getColumnCount() == 2 - && Cursor.FIELD_TYPE_STRING == permCursor.getType(0) - && Cursor.FIELD_TYPE_INTEGER == permCursor.getType(1)) { - String perm = permCursor.getString(0); - Integer granted = permCursor.getInt(1); - if (granted == 1) { - grantedPerms.add(perm); - } else { - ungrantedPerms.add(perm); - } - } - } - permCursor.close(); - - boolean hasUngrantedPerm = false; - for (String wearablePerm : wearablePermissions) { - if (!grantedPerms.contains(wearablePerm)) { - hasUngrantedPerm = true; - if (!ungrantedPerms.contains(wearablePerm)) { - // This is an error condition. This means that the wearable has permissions that - // are not even declared in its host app. This is a developer error. - Log.e(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm - + "\" that is not defined in the host application's manifest."); - } else { - Log.w(TAG, "Wearable " + packageName + " has a permission \"" + wearablePerm + - "\" that is not granted in the host application."); - } - } - } - return hasUngrantedPerm; - } - - /** Finishes the service after fulfilling obligation to call startForeground. */ - private void finishServiceEarly(int startId) { - Pair<Integer, Notification> notifPair = buildNotification( - getApplicationContext().getPackageName(), ""); - startForeground(notifPair.first, notifPair.second); - finishService(null, startId); - } - - private void finishService(PowerManager.WakeLock lock, int startId) { - if (lock != null && lock.isHeld()) { - lock.release(); - } - stopSelf(startId); - } - - private synchronized PowerManager.WakeLock getLock(Context context) { - if (lockStatic == null) { - PowerManager mgr = - (PowerManager) context.getSystemService(Context.POWER_SERVICE); - lockStatic = mgr.newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK, context.getClass().getSimpleName()); - lockStatic.setReferenceCounted(true); - } - return lockStatic; - } - - private class PackageInstallListener implements PackageInstallerImpl.InstallListener { - private Context mContext; - private PowerManager.WakeLock mWakeLock; - private int mStartId; - private String mApplicationPackageName; - private PackageInstallListener(Context context, PowerManager.WakeLock wakeLock, - int startId, String applicationPackageName) { - mContext = context; - mWakeLock = wakeLock; - mStartId = startId; - mApplicationPackageName = applicationPackageName; - } - - @Override - public void installBeginning() { - Log.i(TAG, "Package " + mApplicationPackageName + " is being installed."); - } - - @Override - public void installSucceeded() { - try { - Log.i(TAG, "Package " + mApplicationPackageName + " was installed."); - - // Delete tempFile from the file system. - File tempFile = WearPackageUtil.getTemporaryFile(mContext, mApplicationPackageName); - if (tempFile != null) { - tempFile.delete(); - } - } finally { - finishService(mWakeLock, mStartId); - } - } - - @Override - public void installFailed(int errorCode, String errorDesc) { - Log.e(TAG, "Package install failed " + mApplicationPackageName - + ", errorCode " + errorCode); - finishService(mWakeLock, mStartId); - } - } - - private synchronized Pair<Integer, Notification> buildNotification(final String packageName, - final String title) { - int notifId; - if (mNotifIdMap.containsKey(packageName)) { - notifId = mNotifIdMap.get(packageName); - } else { - notifId = mInstallNotificationId++; - mNotifIdMap.put(packageName, notifId); - } - - if (mNotificationChannel == null) { - mNotificationChannel = new NotificationChannel(WEAR_APPS_CHANNEL, - getString(R.string.wear_app_channel), NotificationManager.IMPORTANCE_MIN); - NotificationManager notificationManager = getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(mNotificationChannel); - } - return new Pair<>(notifId, new Notification.Builder(this, WEAR_APPS_CHANNEL) - .setSmallIcon(R.drawable.ic_file_download) - .setContentTitle(title) - .build()); - } - - private void getLabelAndUpdateNotification(String packageName, String title) { - // Update notification since we have a label now. - NotificationManager notificationManager = getSystemService(NotificationManager.class); - Pair<Integer, Notification> notifPair = buildNotification(packageName, title); - notificationManager.notify(notifPair.first, notifPair.second); - } -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java deleted file mode 100644 index 6a9145db9a06..000000000000 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageUtil.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.packageinstaller.wear; - -import android.content.Context; -import android.net.Uri; -import android.os.ParcelFileDescriptor; -import android.system.ErrnoException; -import android.system.Os; -import android.text.TextUtils; -import android.util.Log; - -import org.tukaani.xz.LZMAInputStream; -import org.tukaani.xz.XZInputStream; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; - -public class WearPackageUtil { - private static final String TAG = "WearablePkgInstaller"; - - private static final String COMPRESSION_LZMA = "lzma"; - private static final String COMPRESSION_XZ = "xz"; - - public static File getTemporaryFile(Context context, String packageName) { - try { - File newFileDir = new File(context.getFilesDir(), "tmp"); - newFileDir.mkdirs(); - Os.chmod(newFileDir.getAbsolutePath(), 0771); - File newFile = new File(newFileDir, packageName + ".apk"); - return newFile; - } catch (ErrnoException e) { - Log.e(TAG, "Failed to open.", e); - return null; - } - } - - public static File getIconFile(final Context context, final String packageName) { - try { - File newFileDir = new File(context.getFilesDir(), "images/icons"); - newFileDir.mkdirs(); - Os.chmod(newFileDir.getAbsolutePath(), 0771); - return new File(newFileDir, packageName + ".icon"); - } catch (ErrnoException e) { - Log.e(TAG, "Failed to open.", e); - return null; - } - } - - /** - * In order to make sure that the Wearable Asset Manager has a reasonable apk that can be used - * by the PackageManager, we will parse it before sending it to the PackageManager. - * Unfortunately, ParsingPackageUtils needs a file to parse. So, we have to temporarily convert - * the fd to a File. - * - * @param context - * @param fd FileDescriptor to convert to File - * @param packageName Name of package, will define the name of the file - * @param compressionAlg Can be null. For ALT mode the APK will be compressed. We will - * decompress it here - */ - public static File getFileFromFd(Context context, ParcelFileDescriptor fd, - String packageName, String compressionAlg) { - File newFile = getTemporaryFile(context, packageName); - if (fd == null || fd.getFileDescriptor() == null) { - return null; - } - InputStream fr = new ParcelFileDescriptor.AutoCloseInputStream(fd); - try { - if (TextUtils.equals(compressionAlg, COMPRESSION_XZ)) { - fr = new XZInputStream(fr); - } else if (TextUtils.equals(compressionAlg, COMPRESSION_LZMA)) { - fr = new LZMAInputStream(fr); - } - } catch (IOException e) { - Log.e(TAG, "Compression was set to " + compressionAlg + ", but could not decode ", e); - return null; - } - - int nRead; - byte[] data = new byte[1024]; - try { - final FileOutputStream fo = new FileOutputStream(newFile); - while ((nRead = fr.read(data, 0, data.length)) != -1) { - fo.write(data, 0, nRead); - } - fo.flush(); - fo.close(); - Os.chmod(newFile.getAbsolutePath(), 0644); - return newFile; - } catch (IOException e) { - Log.e(TAG, "Reading from Asset FD or writing to temp file failed ", e); - return null; - } catch (ErrnoException e) { - Log.e(TAG, "Could not set permissions on file ", e); - return null; - } finally { - try { - fr.close(); - } catch (IOException e) { - Log.e(TAG, "Failed to close the file from FD ", e); - } - } - } - - /** - * @return com.google.com from expected formats like - * Uri: package:com.google.com, package:/com.google.com, package://com.google.com - */ - public static String getSanitizedPackageName(Uri packageUri) { - String packageName = packageUri.getEncodedSchemeSpecificPart(); - if (packageName != null) { - return packageName.replaceAll("^/+", ""); - } - return packageName; - } -} diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index d6cbf2a0f581..0cb85d8638b0 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -64,6 +64,7 @@ android_library { srcs: [ "src/**/*.java", "src/**/*.kt", + "src/**/I*.aidl", ], } diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 8666584e0972..38c871c698c9 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -92,3 +92,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "volume_dialog_audio_sharing_fix" + namespace: "cross_device_experiences" + description: "Gates whether to show separate volume bars during audio sharing" + bug: "336716411" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreference.java new file mode 100644 index 000000000000..1cbb8b4faaa8 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreference.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +/** + * A data class representing an action/switch preference. The preference could be one of the four + * following forms: 1. Texted row with action to jump to another page 2. Texted row without action + * 3. Texted row with action and switch 4. Texted row with switch + */ +public class ActionSwitchPreference extends DeviceSettingPreference implements Parcelable { + private final String mTitle; + private final String mSummary; + private final Bitmap mIcon; + private final Intent mIntent; + private final boolean mHasSwitch; + private final boolean mChecked; + private final boolean mIsAllowedChangingState; + private final Bundle mExtras; + + ActionSwitchPreference( + String title, + @Nullable String summary, + @Nullable Bitmap icon, + @Nullable Intent intent, + boolean hasSwitch, + boolean checked, + boolean allowChangingState, + @NonNull Bundle extras) { + super(DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH); + validate(title); + mTitle = title; + mSummary = summary; + mIcon = icon; + mIntent = intent; + mHasSwitch = hasSwitch; + mChecked = checked; + mIsAllowedChangingState = allowChangingState; + mExtras = extras; + } + + private static void validate(String title) { + if (Objects.isNull(title)) { + throw new IllegalArgumentException("Title must be set"); + } + } + + /** + * Reads an {@link ActionSwitchPreference} instance from {@link Parcel} + * @param in The parcel to read from + * @return The instance read + */ + @NonNull + public static ActionSwitchPreference readFromParcel(@NonNull Parcel in) { + String title = in.readString(); + String summary = in.readString(); + Bitmap icon = in.readParcelable(Bitmap.class.getClassLoader()); + Intent intent = in.readParcelable(Intent.class.getClassLoader()); + boolean hasSwitch = in.readBoolean(); + boolean checked = in.readBoolean(); + boolean allowChangingState = in.readBoolean(); + Bundle extras = in.readBundle(Bundle.class.getClassLoader()); + return new ActionSwitchPreference( + title, summary, icon, intent, hasSwitch, checked, allowChangingState, extras); + } + + public static final Creator<ActionSwitchPreference> CREATOR = + new Creator<>() { + @Override + @NonNull + public ActionSwitchPreference createFromParcel(@NonNull Parcel in) { + in.readInt(); + return readFromParcel(in); + } + + @Override + @NonNull + public ActionSwitchPreference[] newArray(int size) { + return new ActionSwitchPreference[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(mTitle); + dest.writeString(mSummary); + dest.writeParcelable(mIcon, flags); + dest.writeParcelable(mIntent, flags); + dest.writeBoolean(mHasSwitch); + dest.writeBoolean(mChecked); + dest.writeBoolean(mIsAllowedChangingState); + dest.writeBundle(mExtras); + } + + /** Builder class for {@link ActionSwitchPreference}. */ + public static final class Builder { + private String mTitle; + private String mSummary; + private Bitmap mIcon; + private Intent mIntent; + private boolean mHasSwitch; + private boolean mChecked; + private boolean mIsAllowedChangingState; + private Bundle mExtras = Bundle.EMPTY; + + /** + * Sets the title of the preference. + * + * @param title The title of the preference. + * @return Returns the Builder object. + */ + @NonNull + public Builder setTitle(@NonNull String title) { + mTitle = title; + return this; + } + + /** + * Sets the summary of the preference, optional. + * + * @param summary The preference summary. + * @return Returns the Builder object. + */ + @NonNull + public Builder setSummary(@Nullable String summary) { + mSummary = summary; + return this; + } + + /** + * Sets the icon to be displayed on the left of the preference, optional. + * + * @param icon The icon. + * @return Returns the Builder object. + */ + @NonNull + public Builder setIcon(@Nullable Bitmap icon) { + mIcon = icon; + return this; + } + + /** + * Sets the Intent to launch when the preference is clicked, optional. + * + * @param intent The Intent. + * @return Returns the Builder object. + */ + @NonNull + public Builder setIntent(@Nullable Intent intent) { + mIntent = intent; + return this; + } + + /** + * Sets whether the preference will contain a switch. + * + * @param hasSwitch Whether the preference contains a switch. + * @return Returns the Builder object. + */ + @NonNull + public Builder setHasSwitch(boolean hasSwitch) { + mHasSwitch = hasSwitch; + return this; + } + + /** + * Sets the state of the preference. + * + * @param checked Whether the switch is checked. + * @return Returns the Builder object. + */ + @NonNull + public Builder setChecked(boolean checked) { + mChecked = checked; + return this; + } + + /** + * Sets whether state can be changed by user. + * + * @param allowChangingState Whether user is allowed to change state. + * @return Returns the Builder object. + */ + @NonNull + public Builder setAllowedChangingState(boolean allowChangingState) { + mIsAllowedChangingState = allowChangingState; + return this; + } + + /** + * Sets the extras bundle. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds the {@link ActionSwitchPreference} object. + * + * @return Returns the built {@link ActionSwitchPreference} object. + */ + @NonNull + public ActionSwitchPreference build() { + return new ActionSwitchPreference( + mTitle, + mSummary, + mIcon, + mIntent, + mHasSwitch, + mChecked, + mIsAllowedChangingState, + mExtras); + } + } + + /** + * Gets the title of the preference. + * + * @return Returns the title of the preference. + */ + @NonNull + public String getTitle() { + return mTitle; + } + + /** + * Gets the summary of the preference. + * + * @return Returns the summary of the preference. + */ + @Nullable + public String getSummary() { + return mSummary; + } + + /** + * Gets the icon of the preference. + * + * @return Returns the icon of the preference. + */ + @Nullable + public Bitmap getIcon() { + return mIcon; + } + + /** + * Gets the Intent to launch when the preference is clicked. + * + * @return Returns the intent to launch. + */ + @Nullable + public Intent getIntent() { + return mIntent; + } + + /** + * Whether the preference contains a switch. + * + * @return Whether the preference contains a switch. + */ + public boolean hasSwitch() { + return mHasSwitch; + } + + /** + * Whether the switch is checked. + * + * @return Whether the switch is checked. + */ + public boolean getChecked() { + return mChecked; + } + + /** + * Gets whether the state can be changed by user. + * + * @return Whether the state can be changed by user. + */ + public boolean isAllowedChangingState() { + return mIsAllowedChangingState; + } + + /** + * Gets the extras bundle. + * + * @return The extra bundle. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceState.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceState.java new file mode 100644 index 000000000000..91c1a59fa6f2 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceState.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +/** A data class representing the state of an action/switch preference. */ +public class ActionSwitchPreferenceState extends DeviceSettingPreferenceState + implements Parcelable { + private final boolean mChecked; + private final Bundle mExtras; + + ActionSwitchPreferenceState(boolean checked, @NonNull Bundle extras) { + super(DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH); + mChecked = checked; + mExtras = extras; + } + + /** + * Reads an {@link ActionSwitchPreferenceState} instance from {@link Parcel} + * @param in The parcel to read from + * @return The instance read + */ + @NonNull + public static ActionSwitchPreferenceState readFromParcel(@NonNull Parcel in) { + boolean checked = in.readBoolean(); + Bundle extras = in.readBundle(Bundle.class.getClassLoader()); + return new ActionSwitchPreferenceState(checked, extras); + } + + public static final Creator<ActionSwitchPreferenceState> CREATOR = + new Creator<>() { + @Override + @NonNull + public ActionSwitchPreferenceState createFromParcel(@NonNull Parcel in) { + in.readInt(); + return readFromParcel(in); + } + + @Override + @NonNull + public ActionSwitchPreferenceState[] newArray(int size) { + return new ActionSwitchPreferenceState[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeBoolean(mChecked); + dest.writeBundle(mExtras); + } + + /** Builder class for {@link ActionSwitchPreferenceState}. */ + public static final class Builder { + private boolean mChecked; + private Bundle mExtras = Bundle.EMPTY; + + public Builder() {} + + /** + * Sets the state of the preference. + * + * @param checked Whether the switch is checked. + * @return Returns the Builder object. + */ + @NonNull + public Builder setChecked(boolean checked) { + mChecked = checked; + return this; + } + + /** + * Sets the extras bundle. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds the object. + * + * @return Returns the built object. + */ + @NonNull + public ActionSwitchPreferenceState build() { + return new ActionSwitchPreferenceState(mChecked, mExtras); + } + } + + /** + * Whether the switch is checked. + * + * @return Whether the switch is checked. + */ + public boolean getChecked() { + return mChecked; + } + + /** + * Gets the extras bundle. + * + * @return The extra bundle. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.aidl new file mode 100644 index 000000000000..acbaf2df1d2c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +parcelable DeviceInfo;
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.java new file mode 100644 index 000000000000..52e520ea8835 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfo.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** A data class representing a bluetooth device. */ +public class DeviceInfo implements Parcelable { + private final String mBluetoothAddress; + private final Bundle mExtras; + + DeviceInfo(String bluetoothAddress, Bundle extras) { + validate(bluetoothAddress); + mBluetoothAddress = bluetoothAddress; + mExtras = extras; + } + + private static void validate(String bluetoothAddress) { + if (Objects.isNull(bluetoothAddress)) { + throw new IllegalArgumentException("Bluetooth address must be set"); + } + } + + /** Read a {@link DeviceInfo} instance from {@link Parcel} */ + @NonNull + public static DeviceInfo readFromParcel(@NonNull Parcel in) { + String bluetoothAddress = in.readString(); + Bundle extras = in.readBundle(Bundle.class.getClassLoader()); + return new DeviceInfo(bluetoothAddress, extras); + } + + public static final Creator<DeviceInfo> CREATOR = + new Creator<>() { + @Override + @NonNull + public DeviceInfo createFromParcel(@NonNull Parcel in) { + return readFromParcel(in); + } + + @Override + @NonNull + public DeviceInfo[] newArray(int size) { + return new DeviceInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mBluetoothAddress); + dest.writeBundle(mExtras); + } + + /** Builder class for {@link DeviceInfo}. */ + public static final class Builder { + private String mBluetoothAddress; + private Bundle mExtras = Bundle.EMPTY; + + /** + * Sets the bluetooth address of the device, from {@link + * android.bluetooth.BluetoothDevice#getAddress()}. + * + * @param bluetoothAddress The bluetooth address. + * @return Returns the Builder object. + */ + @NonNull + public Builder setBluetoothAddress(@NonNull String bluetoothAddress) { + mBluetoothAddress = bluetoothAddress; + return this; + } + + /** + * Sets the extras bundle. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds the {@link DeviceInfo} object. + * + * @return Returns the built {@link DeviceInfo} object. + */ + @NonNull + public DeviceInfo build() { + return new DeviceInfo(mBluetoothAddress, mExtras); + } + } + + /** + * Gets the bluetooth address of the device. + * + * @return The bluetooth address from {@link android.bluetooth.BluetoothDevice#getAddress()}. + */ + @NonNull + public String getBluetoothAddress() { + return mBluetoothAddress; + } + + /** + * Gets the extras bundle. + * + * @return The extra bundle. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.aidl new file mode 100644 index 000000000000..043cae3a576a --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +parcelable DeviceSetting;
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.java new file mode 100644 index 000000000000..dc219a9dfcf4 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSetting.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** A data class representing a device setting item in bluetooth device details page. */ +public final class DeviceSetting implements Parcelable { + @DeviceSettingId private final int mSettingId; + private final DeviceSettingPreference mPreference; + private final Bundle mExtras; + + DeviceSetting( + int settingId, @NonNull DeviceSettingPreference preference, @NonNull Bundle extras) { + validate(preference); + mSettingId = settingId; + mPreference = preference; + mExtras = extras; + } + + private static void validate(DeviceSettingPreference preference) { + if (Objects.isNull(preference)) { + throw new IllegalArgumentException("Preference must be set"); + } + } + + /** Read a {@link DeviceSetting} instance from {@link Parcel} */ + @NonNull + public static DeviceSetting readFromParcel(@NonNull Parcel in) { + int settingId = in.readInt(); + Bundle extras = in.readBundle(Bundle.class.getClassLoader()); + DeviceSettingPreference settingPreference = DeviceSettingPreference.readFromParcel(in); + return new DeviceSetting(settingId, settingPreference, extras); + } + + public static final Creator<DeviceSetting> CREATOR = + new Creator<>() { + @Override + @NonNull + public DeviceSetting createFromParcel(@NonNull Parcel in) { + return readFromParcel(in); + } + + @Override + @NonNull + public DeviceSetting[] newArray(int size) { + return new DeviceSetting[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mSettingId); + dest.writeBundle(mExtras); + mPreference.writeToParcel(dest, flags); + } + + /** Builder class for {@link DeviceSetting}. */ + public static final class Builder { + private int mSettingId; + private DeviceSettingPreference mPreference; + private Bundle mExtras = Bundle.EMPTY; + + public Builder() {} + + /** + * Sets the setting ID, as defined by IntDef {@link DeviceSettingId}. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setSettingId(@DeviceSettingId int settingId) { + mSettingId = settingId; + return this; + } + + /** + * Sets the setting preference. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setPreference(@NonNull DeviceSettingPreference settingPreference) { + mPreference = settingPreference; + return this; + } + + /** + * Sets the extras bundle. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** Build the object. */ + @NonNull + public DeviceSetting build() { + return new DeviceSetting(mSettingId, mPreference, mExtras); + } + } + + /** + * Gets the setting ID as defined by IntDef {@link DeviceSettingId}. + * + * @return Returns the setting ID. + */ + @DeviceSettingId + public int getSettingId() { + return mSettingId; + } + + /** + * Gets the setting preference. + * + * @return Returns the setting preference. + */ + @NonNull + public DeviceSettingPreference getPreference() { + return mPreference; + } + + /** + * Gets the extras Bundle. + * + * @return Returns a Bundle object. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java new file mode 100644 index 000000000000..20a0339b9182 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +@IntDef( + value = { + DeviceSettingId.DEVICE_SETTING_ID_UNKNOWN, + DeviceSettingId.DEVICE_SETTING_ID_HEADER, + DeviceSettingId.DEVICE_SETTING_ID_ADVANCED_HEADER, + DeviceSettingId.DEVICE_SETTING_ID_LE_AUDIO_HEADER, + DeviceSettingId.DEVICE_SETTING_ID_HEARING_AID_PAIR_OTHER_BUTTON, + DeviceSettingId.DEVICE_SETTING_ID_HEARING_AID_SPACE_LAYOUT, + DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, + DeviceSettingId.DEVICE_SETTING_ID_DEVICE_STYLUS, + DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_EXTRA_CONTROL, + DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_DEVICE_SLICE_CATEGORY, + DeviceSettingId.DEVICE_SETTING_ID_DEVICE_COMPANION_APPS, + DeviceSettingId.DEVICE_SETTING_ID_HEARING_DEVICE_GROUP, + DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_AUDIO_DEVICE_TYPE_GROUP, + DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_GROUP, + DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES, + DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_EXTRA_OPTIONS, + DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_RELATED_TOOLS, + DeviceSettingId.DEVICE_SETTING_ID_DATA_SYNC_GROUP, + DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, + DeviceSettingId.DEVICE_SETTING_ID_DEVICE_DETAILS_FOOTER, + DeviceSettingId.DEVICE_SETTING_ID_ANC, + }, + open = true) +public @interface DeviceSettingId { + /** Device setting ID is unknown. */ + int DEVICE_SETTING_ID_UNKNOWN = 0; + + /** Device setting ID for header. */ + int DEVICE_SETTING_ID_HEADER = 1; + + /** Device setting ID for advanced header. */ + int DEVICE_SETTING_ID_ADVANCED_HEADER = 2; + + /** Device setting ID for LeAudio header. */ + int DEVICE_SETTING_ID_LE_AUDIO_HEADER = 3; + + /** Device setting ID for hearing aid “pair other” button. */ + int DEVICE_SETTING_ID_HEARING_AID_PAIR_OTHER_BUTTON = 4; + + /** Device setting ID for hearing aid space layout. */ + int DEVICE_SETTING_ID_HEARING_AID_SPACE_LAYOUT = 5; + + /** Device setting ID for action buttons(Forget, Connect/Disconnect). */ + int DEVICE_SETTING_ID_ACTION_BUTTONS = 6; + + /** Device setting ID for stylus device. */ + int DEVICE_SETTING_ID_DEVICE_STYLUS = 7; + + /** Device setting ID for bluetooth extra control. */ + int DEVICE_SETTING_ID_BLUETOOTH_EXTRA_CONTROL = 8; + + /** Device setting ID for bluetooth device slice category. */ + int DEVICE_SETTING_ID_BLUETOOTH_DEVICE_SLICE_CATEGORY = 9; + + /** Device setting ID for device companion apps. */ + int DEVICE_SETTING_ID_DEVICE_COMPANION_APPS = 10; + + /** Device setting ID for hearing device group. */ + int DEVICE_SETTING_ID_HEARING_DEVICE_GROUP = 11; + + /** Device setting ID for bluetooth audio device type group. */ + int DEVICE_SETTING_ID_BLUETOOTH_AUDIO_DEVICE_TYPE_GROUP = 12; + + /** Device setting ID for spatial audio group. */ + int DEVICE_SETTING_ID_SPATIAL_AUDIO_GROUP = 13; + + /** Device setting ID for bluetooth profiles. */ + int DEVICE_SETTING_ID_BLUETOOTH_PROFILES = 14; + + /** Device setting ID for bluetooth extra options. */ + int DEVICE_SETTING_ID_BLUETOOTH_EXTRA_OPTIONS = 15; + + /** Device setting ID for bluetooth related tools. */ + int DEVICE_SETTING_ID_BLUETOOTH_RELATED_TOOLS = 16; + + /** Device setting ID for data sync group. */ + int DEVICE_SETTING_ID_DATA_SYNC_GROUP = 17; + + /** Device setting ID for keyboard settings. */ + int DEVICE_SETTING_ID_KEYBOARD_SETTINGS = 18; + + /** Device setting ID for device details footer. */ + int DEVICE_SETTING_ID_DEVICE_DETAILS_FOOTER = 19; + + /** Device setting ID for ANC. */ + int DEVICE_SETTING_ID_ANC = 1001; +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt new file mode 100644 index 000000000000..9ee33b0dbffe --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable + +/** + * A data class representing a device settings item in bluetooth device details config. + * + * @property settingId The setting ID of the item, as defined by IntDef [DeviceSettingId]. + * @property packageName The package name for service binding. + * @property className The class name for service binding. + * @property intentAction The intent action for service binding. + * @property extras Extra bundle + */ +data class DeviceSettingItem( + @DeviceSettingId val settingId: Int, + val packageName: String, + val className: String, + val intentAction: String, + val extras: Bundle = Bundle.EMPTY, +) : Parcelable { + + override fun describeContents(): Int = 0 + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.run { + writeInt(settingId) + writeString(packageName) + writeString(className) + writeString(intentAction) + writeBundle(extras) + } + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator<DeviceSettingItem> = + object : Parcelable.Creator<DeviceSettingItem> { + override fun createFromParcel(parcel: Parcel) = + parcel.run { + DeviceSettingItem( + settingId = readInt(), + packageName = readString() ?: "", + className = readString() ?: "", + intentAction = readString() ?: "", + extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY, + ) + } + + override fun newArray(size: Int): Array<DeviceSettingItem?> { + return arrayOfNulls(size) + } + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java new file mode 100644 index 000000000000..790939a18a0c --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +/** An abstract class representing a device setting preference. */ +public abstract class DeviceSettingPreference { + @DeviceSettingType private final int mSettingType; + + public static final DeviceSettingPreference UNKNOWN = + new DeviceSettingPreference(DeviceSettingType.DEVICE_SETTING_TYPE_UNKNOWN) {}; + + protected DeviceSettingPreference(@DeviceSettingType int settingType) { + mSettingType = settingType; + } + + /** Read a {@link DeviceSettingPreference} instance from {@link Parcel} */ + @NonNull + public static DeviceSettingPreference readFromParcel(@NonNull Parcel in) { + int type = in.readInt(); + switch (type) { + case DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH: + return ActionSwitchPreference.readFromParcel(in); + case DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE: + return MultiTogglePreference.readFromParcel(in); + default: + return UNKNOWN; + } + } + + /** Writes the instance to {@link Parcel}. */ + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mSettingType); + } + + /** + * Gets the setting type, as defined by IntDef {@link DeviceSettingType}. + * + * @return the setting type. + */ + @DeviceSettingType + public int getSettingType() { + return mSettingType; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreferenceState.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreferenceState.java new file mode 100644 index 000000000000..a982af709f69 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreferenceState.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.os.Parcel; + +import androidx.annotation.NonNull; + +/** An abstract class representing a device setting preference state. */ +public abstract class DeviceSettingPreferenceState { + @DeviceSettingType private final int mSettingType; + + public static final DeviceSettingPreferenceState UNKNOWN = + new DeviceSettingPreferenceState(DeviceSettingType.DEVICE_SETTING_TYPE_UNKNOWN) {}; + + protected DeviceSettingPreferenceState(@DeviceSettingType int settingType) { + mSettingType = settingType; + } + + /** Reads a {@link DeviceSettingPreferenceState} from {@link Parcel}. */ + @NonNull + public static DeviceSettingPreferenceState readFromParcel(@NonNull Parcel in) { + int type = in.readInt(); + switch (type) { + case DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH: + return ActionSwitchPreferenceState.readFromParcel(in); + case DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE: + return MultiTogglePreferenceState.readFromParcel(in); + default: + return UNKNOWN; + } + } + + /** Writes the object to parcel. */ + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mSettingType); + } + + /** + * Gets the setting type, as defined by IntDef {@link DeviceSettingType}. + * + * @return The setting type. + */ + @DeviceSettingType + public int getSettingType() { + return mSettingType; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.aidl new file mode 100644 index 000000000000..61429a63128f --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +parcelable DeviceSettingState;
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.java new file mode 100644 index 000000000000..63fd4eb425c0 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingState.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** A data class representing a device setting state. */ +public class DeviceSettingState implements Parcelable { + @DeviceSettingId private final int mSettingId; + private final DeviceSettingPreferenceState mPreferenceState; + private final Bundle mExtras; + + DeviceSettingState( + @DeviceSettingId int settingId, + @NonNull DeviceSettingPreferenceState preferenceState, + @NonNull Bundle extras) { + validate(preferenceState); + mSettingId = settingId; + mPreferenceState = preferenceState; + mExtras = extras; + } + + private static void validate(DeviceSettingPreferenceState preferenceState) { + if (Objects.isNull(preferenceState)) { + throw new IllegalArgumentException("PreferenceState must be set"); + } + } + + /** Reads a {@link DeviceSettingState} from {@link Parcel}. */ + @NonNull + public static DeviceSettingState readFromParcel(@NonNull Parcel in) { + int settingId = in.readInt(); + Bundle extra = in.readBundle(Bundle.class.getClassLoader()); + DeviceSettingPreferenceState preferenceState = + DeviceSettingPreferenceState.readFromParcel(in); + return new DeviceSettingState(settingId, preferenceState, extra); + } + + public static final Creator<DeviceSettingState> CREATOR = + new Creator<>() { + @Override + @NonNull + public DeviceSettingState createFromParcel(@NonNull Parcel in) { + return readFromParcel(in); + } + + @Override + @NonNull + public DeviceSettingState[] newArray(int size) { + return new DeviceSettingState[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + /** Writes the instance to {@link Parcel}. */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mSettingId); + dest.writeBundle(mExtras); + mPreferenceState.writeToParcel(dest, flags); + } + + /** Builder class for {@link DeviceSettingState}. */ + public static final class Builder { + private int mSettingId; + private DeviceSettingPreferenceState mSettingPreferenceState; + private Bundle mExtras = Bundle.EMPTY; + + public Builder() {} + + /** + * Sets the setting ID, as defined by IntDef {@link DeviceSettingId}. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setSettingId(@DeviceSettingId int settingId) { + mSettingId = settingId; + return this; + } + + /** + * Sets the setting preference state. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setPreferenceState( + @NonNull DeviceSettingPreferenceState settingPreferenceState) { + mSettingPreferenceState = settingPreferenceState; + return this; + } + + /** + * Sets the extras bundle. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** Build the object. */ + @NonNull + public DeviceSettingState build() { + return new DeviceSettingState(mSettingId, mSettingPreferenceState, mExtras); + } + } + + /** + * Gets the setting ID, as defined by IntDef {@link DeviceSettingId}. + * + * @return the setting ID. + */ + @DeviceSettingId + public int getSettingId() { + return mSettingId; + } + + /** + * Gets the preference state of the setting. + * + * @return the setting preference state. + */ + @NonNull + public DeviceSettingPreferenceState getPreferenceState() { + return mPreferenceState; + } + + /** + * Gets the extras Bundle. + * + * @return Returns a Bundle object. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.java new file mode 100644 index 000000000000..ee4d90fe4171 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingType.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +@IntDef( + value = { + DeviceSettingType.DEVICE_SETTING_TYPE_UNKNOWN, + DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH, + DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE, + }, + open = true) +public @interface DeviceSettingType { + /** Device setting type is unknown. */ + int DEVICE_SETTING_TYPE_UNKNOWN = 0; + + /** Device setting type is action/switch preference. */ + int DEVICE_SETTING_TYPE_ACTION_SWITCH = 1; + + /** Device setting type is multi-toggle preference. */ + int DEVICE_SETTING_TYPE_MULTI_TOGGLE = 2; +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.aidl new file mode 100644 index 000000000000..3201d13cd92e --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +parcelable DeviceSettingsConfig;
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt new file mode 100644 index 000000000000..c8a2e9ca3af1 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfig.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable + +/** + * A data class representing a bluetooth device details config. + * + * @property mainContentItems The setting items to be shown in main page. + * @property moreSettingsItems The setting items to be shown in more settings page. + * @property moreSettingsFooter The footer in more settings page. + * @property extras Extra bundle + */ +data class DeviceSettingsConfig( + val mainContentItems: List<DeviceSettingItem>, + val moreSettingsItems: List<DeviceSettingItem>, + val moreSettingsFooter: String, + val extras: Bundle = Bundle.EMPTY, +) : Parcelable { + + override fun describeContents(): Int = 0 + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.run { + writeTypedList(mainContentItems) + writeTypedList(moreSettingsItems) + writeString(moreSettingsFooter) + writeBundle(extras) + } + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator<DeviceSettingsConfig> = + object : Parcelable.Creator<DeviceSettingsConfig> { + override fun createFromParcel(parcel: Parcel): DeviceSettingsConfig = + parcel.run { + DeviceSettingsConfig( + mainContentItems = + arrayListOf<DeviceSettingItem>().also { + readTypedList(it, DeviceSettingItem.CREATOR) + }, + moreSettingsItems = + arrayListOf<DeviceSettingItem>().also { + readTypedList(it, DeviceSettingItem.CREATOR) + }, + moreSettingsFooter = readString()!!, + extras = readBundle((Bundle::class.java.classLoader))!!, + ) + } + + override fun newArray(size: Int): Array<DeviceSettingsConfig?> { + return arrayOfNulls(size) + } + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsListener.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsListener.aidl new file mode 100644 index 000000000000..385a780b01f3 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsListener.aidl @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import com.android.settingslib.bluetooth.devicesettings.DeviceSetting; + +interface IDeviceSettingsListener { + oneway void onDeviceSettingsChanged(in List<DeviceSetting> settings) = 0; +}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl new file mode 100644 index 000000000000..d5efac9d0336 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/IDeviceSettingsProviderService.aidl @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import com.android.settingslib.bluetooth.devicesettings.DeviceInfo; +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingState; +import com.android.settingslib.bluetooth.devicesettings.IDeviceSettingsListener; + +oneway interface IDeviceSettingsProviderService { + void registerDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); + void unregisterDeviceSettingsListener(in DeviceInfo device, in IDeviceSettingsListener callback); + void updateDeviceSettings(in DeviceInfo device, in DeviceSettingState params); +}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java new file mode 100644 index 000000000000..01bb6f013d16 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreference.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** A data class representing a multi-toggle preference. */ +public class MultiTogglePreference extends DeviceSettingPreference implements Parcelable { + private final String mTitle; + private final ImmutableList<ToggleInfo> mToggleInfos; + private final int mState; + private final boolean mIsAllowedChangingState; + private final Bundle mExtras; + + MultiTogglePreference( + @NonNull String title, + List<ToggleInfo> toggleInfos, + int state, + boolean allowChangingState, + Bundle extras) { + super(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE); + validate(title, state); + mTitle = title; + mToggleInfos = ImmutableList.copyOf(toggleInfos); + mState = state; + mIsAllowedChangingState = allowChangingState; + mExtras = extras; + } + + private static void validate(String title, int state) { + if (Objects.isNull(title)) { + throw new IllegalArgumentException("Title must be set"); + } + if (state < 0) { + throw new IllegalArgumentException("State must be a non-negative integer"); + } + } + + /** Read a {@link MultiTogglePreference} from {@link Parcel}. */ + @NonNull + public static MultiTogglePreference readFromParcel(@NonNull Parcel in) { + String title = in.readString(); + List<ToggleInfo> toggleInfos = new ArrayList<>(); + in.readTypedList(toggleInfos, ToggleInfo.CREATOR); + int state = in.readInt(); + boolean allowChangingState = in.readBoolean(); + Bundle extras = in.readBundle(Bundle.class.getClassLoader()); + return new MultiTogglePreference(title, toggleInfos, state, allowChangingState, extras); + } + + public static final Creator<MultiTogglePreference> CREATOR = + new Creator<>() { + @Override + @NonNull + public MultiTogglePreference createFromParcel(@NonNull Parcel in) { + in.readInt(); + return readFromParcel(in); + } + + @Override + @NonNull + public MultiTogglePreference[] newArray(int size) { + return new MultiTogglePreference[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString(mTitle); + dest.writeTypedList(mToggleInfos, flags); + dest.writeInt(mState); + dest.writeBoolean(mIsAllowedChangingState); + dest.writeBundle(mExtras); + } + + /** Builder class for {@link MultiTogglePreference}. */ + public static final class Builder { + private String mTitle; + private ImmutableList.Builder<ToggleInfo> mToggleInfos = new ImmutableList.Builder<>(); + private int mState; + private boolean mAllowChangingState; + private Bundle mExtras = Bundle.EMPTY; + + /** + * Sets the title of the preference. + * + * @param title The title of the preference. + * @return Returns the Builder object. + */ + @NonNull + public Builder setTitle(@NonNull String title) { + mTitle = title; + return this; + } + + /** + * Adds a toggle in the preference. + * + * @param toggleInfo The toggle to add. + * @return Returns the Builder object. + */ + @NonNull + public Builder addToggleInfo(@NonNull ToggleInfo toggleInfo) { + mToggleInfos.add(toggleInfo); + return this; + } + + /** + * Sets the state of the preference. + * + * @param state The index of the enabled toggle. + * @return Returns the Builder object. + */ + @NonNull + public Builder setState(int state) { + mState = state; + return this; + } + + /** + * Sets whether state can be changed by user. + * + * @param allowChangingState Whether user is allowed to change state. + * @return Returns the Builder object. + */ + @NonNull + public Builder setAllowChangingState(boolean allowChangingState) { + mAllowChangingState = allowChangingState; + return this; + } + + /** + * Sets the extras bundle. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds the {@link ToggleInfo} object. + * + * @return Returns the built {@link ToggleInfo} object. + */ + @NonNull + public MultiTogglePreference build() { + return new MultiTogglePreference( + mTitle, mToggleInfos.build(), mState, mAllowChangingState, mExtras); + } + } + + /** + * Gets the title of the preference. + * + * @return The title. + */ + @NonNull + public String getTitle() { + return mTitle; + } + + /** + * Gets the state of the {@link MultiTogglePreference}. + * + * @return Returns the index of the enabled toggle. + */ + public int getState() { + return mState; + } + + /** + * Gets the toggle list in the preference. + * + * @return the toggle list. + */ + @NonNull + public List<ToggleInfo> getToggleInfos() { + return mToggleInfos; + } + + /** + * Gets whether the state can be changed by user. + * + * @return Whether the state can be changed by user. + */ + public boolean isAllowedChangingState() { + return mIsAllowedChangingState; + } + + /** + * Gets the extras Bundle. + * + * @return Returns a Bundle object. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceState.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceState.java new file mode 100644 index 000000000000..239df0b55b22 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceState.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +/** A data class representing a multi-toggle preference state. */ +public class MultiTogglePreferenceState extends DeviceSettingPreferenceState implements Parcelable { + private final int mState; + private final Bundle mExtras; + + MultiTogglePreferenceState(int state, @NonNull Bundle extras) { + super(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE); + mState = state; + mExtras = extras; + } + + /** Reads a {@link MultiTogglePreferenceState} from {@link Parcel}. */ + @NonNull + public static MultiTogglePreferenceState readFromParcel(@NonNull Parcel in) { + int state = in.readInt(); + Bundle extras = in.readBundle(Bundle.class.getClassLoader()); + return new MultiTogglePreferenceState(state, extras); + } + + public static final Creator<MultiTogglePreferenceState> CREATOR = + new Creator<>() { + @Override + @NonNull + public MultiTogglePreferenceState createFromParcel(@NonNull Parcel in) { + in.readInt(); + return readFromParcel(in); + } + + @Override + @NonNull + public MultiTogglePreferenceState[] newArray(int size) { + return new MultiTogglePreferenceState[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mState); + dest.writeBundle(mExtras); + } + + /** Builder class for {@link MultiTogglePreferenceState}. */ + public static final class Builder { + private int mState; + private Bundle mExtras = Bundle.EMPTY; + + public Builder() {} + + /** + * Sets the state of {@link MultiTogglePreference}. + * + * @return Returns the index of enabled toggle. + */ + @NonNull + public Builder setState(int state) { + mState = state; + return this; + } + + /** + * Sets the extras bundle. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** Builds the object. */ + @NonNull + public MultiTogglePreferenceState build() { + return new MultiTogglePreferenceState(mState, mExtras); + } + } + + /** + * Gets the state of the {@link MultiTogglePreference}. + * + * @return Returns the index of the enabled toggle. + */ + public int getState() { + return mState; + } + + /** + * Gets the extras Bundle. + * + * @return Returns a Bundle object. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfo.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfo.java new file mode 100644 index 000000000000..7dcf3aa7239e --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfo.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** A data class representing a toggle in {@link MultiTogglePreference}. */ +public class ToggleInfo implements Parcelable { + private final String mLabel; + private final Bitmap mIcon; + private final Bundle mExtras; + + ToggleInfo(@NonNull String label, @NonNull Bitmap icon, @NonNull Bundle extras) { + validate(label, icon); + mLabel = label; + mIcon = icon; + mExtras = extras; + } + + private static void validate(String label, Bitmap icon) { + if (Objects.isNull(label)) { + throw new IllegalArgumentException("Label must be set"); + } + if (Objects.isNull(icon)) { + throw new IllegalArgumentException("Icon must be set"); + } + } + + /** Read a {@link ToggleInfo} instance from {@link Parcel}. */ + @NonNull + public static ToggleInfo readFromParcel(@NonNull Parcel in) { + String label = in.readString(); + Bitmap icon = in.readParcelable(Bitmap.class.getClassLoader()); + Bundle extras = in.readBundle(Bundle.class.getClassLoader()); + return new ToggleInfo(label, icon, extras); + } + + public static final Creator<ToggleInfo> CREATOR = + new Creator<>() { + @Override + @NonNull + public ToggleInfo createFromParcel(@NonNull Parcel in) { + return readFromParcel(in); + } + + @Override + @NonNull + public ToggleInfo[] newArray(int size) { + return new ToggleInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mLabel); + dest.writeParcelable(mIcon, flags); + dest.writeBundle(mExtras); + } + + /** Builder class for {@link ToggleInfo}. */ + public static final class Builder { + private Bitmap mIcon; + private String mLabel; + private Bundle mExtras = Bundle.EMPTY; + + /** + * Sets the label of the toggle. + * + * @param label The label of the toggle. + * @return Returns the Builder object. + */ + @NonNull + public Builder setLabel(@NonNull String label) { + mLabel = label; + return this; + } + + /** + * Sets the icon of the toggle. + * + * @param icon The icon of the toggle. + * @return Returns the Builder object. + */ + @NonNull + public Builder setIcon(@NonNull Bitmap icon) { + mIcon = icon; + return this; + } + + /** + * Sets the extras bundle. + * + * @return Returns the Builder object. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds the {@link ToggleInfo} object. + * + * @return Returns the built {@link ToggleInfo} object. + */ + @NonNull + public ToggleInfo build() { + return new ToggleInfo(mLabel, mIcon, mExtras); + } + } + + /** + * Gets the label of the toggle. + * + * @return the label to be shown under the toggle + */ + @NonNull + public String getLabel() { + return mLabel; + } + + /** + * Gets the icon of the toggle. + * + * @return the icon in toggle + */ + @NonNull + public Bitmap getIcon() { + return mIcon; + } + + /** + * Gets the extras Bundle. + * + * @return Returns a Bundle object. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceStateTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceStateTest.java new file mode 100644 index 000000000000..e8e1556b8fe7 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceStateTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Bundle; +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class ActionSwitchPreferenceStateTest { + + @Test + public void getMethods() { + ActionSwitchPreferenceState state1 = + new ActionSwitchPreferenceState.Builder() + .setChecked(true) + .setExtras(buildBundle("key1", "value1")) + .build(); + ActionSwitchPreferenceState state2 = + new ActionSwitchPreferenceState.Builder() + .setChecked(false) + .setExtras(buildBundle("key2", "value2")) + .build(); + + assertThat(state1.getChecked()).isTrue(); + assertThat(state2.getChecked()).isFalse(); + assertThat(state1.getExtras().getString("key1")).isEqualTo("value1"); + assertThat(state2.getExtras().getString("key2")).isEqualTo("value2"); + } + + @Test + public void parcelOperation_notChecked() { + ActionSwitchPreferenceState state = + new ActionSwitchPreferenceState.Builder() + .setChecked(false) + .setExtras(buildBundle("key1", "value1")) + .build(); + + ActionSwitchPreferenceState fromParcel = writeAndRead(state); + + assertThat(fromParcel.getChecked()).isEqualTo(state.getChecked()); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(state.getExtras().getString("key1")); + } + + @Test + public void parcelOperation_checked() { + ActionSwitchPreferenceState state = + new ActionSwitchPreferenceState.Builder() + .setChecked(true) + .setExtras(buildBundle("key2", "value2")) + .build(); + + ActionSwitchPreferenceState fromParcel = writeAndRead(state); + + assertThat(fromParcel.getChecked()).isEqualTo(state.getChecked()); + assertThat(fromParcel.getExtras().getString("key2")) + .isEqualTo(state.getExtras().getString("key2")); + } + + private Bundle buildBundle(String key, String value) { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + return bundle; + } + + private ActionSwitchPreferenceState writeAndRead(ActionSwitchPreferenceState state) { + Parcel parcel = Parcel.obtain(); + state.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ActionSwitchPreferenceState fromParcel = + ActionSwitchPreferenceState.CREATOR.createFromParcel(parcel); + return fromParcel; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceTest.java new file mode 100644 index 000000000000..354d0f658544 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ActionSwitchPreferenceTest.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class ActionSwitchPreferenceTest { + + @Test + public void build_withoutTitle_fail() { + assertThrows( + IllegalArgumentException.class, + () -> { + ActionSwitchPreference unused = + new ActionSwitchPreference.Builder().setSummary("summary").build(); + }); + } + + @Test + public void build_withTitle_successfully() { + ActionSwitchPreference unused = + new ActionSwitchPreference.Builder().setTitle("title").build(); + } + + @Test + public void build_withAllFields_successfully() { + ActionSwitchPreference unused = + new ActionSwitchPreference.Builder() + .setTitle("title") + .setSummary("summary") + .setIntent(new Intent("intent_action")) + .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + .setHasSwitch(true) + .setChecked(true) + .setAllowedChangingState(true) + .setExtras(buildBundle("key1", "value1")) + .build(); + } + + @Test + public void getMethods() { + Intent intent = new Intent("intent_action"); + Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + ActionSwitchPreference preference = builder().setIcon(icon).setIntent(intent).build(); + + assertThat(preference.getTitle()).isEqualTo("title"); + assertThat(preference.getSummary()).isEqualTo("summary"); + assertThat(preference.getIcon()).isSameInstanceAs(icon); + assertThat(preference.getIntent()).isSameInstanceAs(intent); + assertThat(preference.hasSwitch()).isTrue(); + assertThat(preference.getChecked()).isTrue(); + assertThat(preference.isAllowedChangingState()).isTrue(); + assertThat(preference.getExtras().getString("key1")).isEqualTo("value1"); + } + + @Test + public void parcelOperation() { + Intent intent = new Intent("intent_action"); + Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + ActionSwitchPreference preference = builder().setIcon(icon).setIntent(intent).build(); + + ActionSwitchPreference fromParcel = writeAndRead(preference); + + assertThat(fromParcel.getTitle()).isEqualTo(preference.getTitle()); + assertThat(fromParcel.getSummary()).isEqualTo(preference.getSummary()); + assertThat(fromParcel.getIcon().sameAs(preference.getIcon())).isTrue(); + assertThat(fromParcel.getIntent().getAction()).isSameInstanceAs("intent_action"); + assertThat(fromParcel.hasSwitch()).isEqualTo(preference.hasSwitch()); + assertThat(fromParcel.getChecked()).isEqualTo(preference.getChecked()); + assertThat(fromParcel.isAllowedChangingState()) + .isEqualTo(preference.isAllowedChangingState()); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(preference.getExtras().getString("key1")); + } + + @Test + public void parcelOperation_noIntent() { + Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + ActionSwitchPreference preference = builder().setIcon(icon).setIntent(null).build(); + + ActionSwitchPreference fromParcel = writeAndRead(preference); + + assertThat(fromParcel.getTitle()).isEqualTo(preference.getTitle()); + assertThat(fromParcel.getSummary()).isEqualTo(preference.getSummary()); + assertThat(fromParcel.getIcon().sameAs(preference.getIcon())).isTrue(); + assertThat(preference.getIntent()).isNull(); + assertThat(fromParcel.hasSwitch()).isEqualTo(preference.hasSwitch()); + assertThat(fromParcel.getChecked()).isEqualTo(preference.getChecked()); + assertThat(fromParcel.isAllowedChangingState()) + .isEqualTo(preference.isAllowedChangingState()); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(preference.getExtras().getString("key1")); + } + + @Test + public void parcelOperation_noIcon() { + Intent intent = new Intent("intent_action"); + ActionSwitchPreference preference = builder().setIcon(null).setIntent(intent).build(); + + ActionSwitchPreference fromParcel = writeAndRead(preference); + + assertThat(fromParcel.getTitle()).isEqualTo(preference.getTitle()); + assertThat(fromParcel.getSummary()).isEqualTo(preference.getSummary()); + assertThat(fromParcel.getIcon()).isNull(); + assertThat(fromParcel.getIntent().getAction()).isSameInstanceAs("intent_action"); + assertThat(fromParcel.hasSwitch()).isEqualTo(preference.hasSwitch()); + assertThat(fromParcel.getChecked()).isEqualTo(preference.getChecked()); + assertThat(fromParcel.isAllowedChangingState()) + .isEqualTo(preference.isAllowedChangingState()); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(preference.getExtras().getString("key1")); + } + + private Bundle buildBundle(String key, String value) { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + return bundle; + } + + private ActionSwitchPreference writeAndRead(ActionSwitchPreference preference) { + Parcel parcel = Parcel.obtain(); + preference.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ActionSwitchPreference fromParcel = ActionSwitchPreference.CREATOR.createFromParcel(parcel); + return fromParcel; + } + + private ActionSwitchPreference.Builder builder() { + return new ActionSwitchPreference.Builder() + .setTitle("title") + .setSummary("summary") + .setHasSwitch(true) + .setChecked(true) + .setAllowedChangingState(true) + .setExtras(buildBundle("key1", "value1")); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfoTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfoTest.java new file mode 100644 index 000000000000..fd5b07508b52 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceInfoTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.os.Bundle; +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class DeviceInfoTest { + @Test + public void build_withoutBluetoothAddress_fail() { + assertThrows( + IllegalArgumentException.class, + () -> { + DeviceInfo unused = + new DeviceInfo.Builder() + .setExtras(buildBundle("key1", "value1")) + .build(); + }); + } + + @Test + public void build_withoutExtra_successfully() { + DeviceInfo unused = new DeviceInfo.Builder().setBluetoothAddress("12:34:56:78").build(); + } + + @Test + public void build_withAllFields_successfully() { + DeviceInfo unused = + new DeviceInfo.Builder() + .setBluetoothAddress("12:34:56:78") + .setExtras(buildBundle("key1", "value1")) + .build(); + } + + @Test + public void getMethods() { + DeviceInfo info = + new DeviceInfo.Builder() + .setBluetoothAddress("12:34:56:78") + .setExtras(buildBundle("key1", "value1")) + .build(); + + assertThat(info.getBluetoothAddress()).isEqualTo("12:34:56:78"); + assertThat(info.getExtras().getString("key1")).isEqualTo("value1"); + } + + @Test + public void parcelOperation() { + DeviceInfo info = + new DeviceInfo.Builder() + .setBluetoothAddress("12:34:56:78") + .setExtras(buildBundle("key1", "value1")) + .build(); + + DeviceInfo fromParcel = writeAndRead(info); + + assertThat(fromParcel.getBluetoothAddress()).isEqualTo(info.getBluetoothAddress()); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(info.getExtras().getString("key1")); + } + + private Bundle buildBundle(String key, String value) { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + return bundle; + } + + private DeviceInfo writeAndRead(DeviceInfo state) { + Parcel parcel = Parcel.obtain(); + state.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DeviceInfo fromParcel = DeviceInfo.CREATOR.createFromParcel(parcel); + return fromParcel; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt new file mode 100644 index 000000000000..56e9b6c27925 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DeviceSettingItemTest { + + @Test + fun parcelOperation() { + val item = + DeviceSettingItem( + settingId = 1, + packageName = "package_name", + className = "class_name", + intentAction = "intent_action", + extras = Bundle().apply { putString("key1", "value1") }, + ) + + val fromParcel = writeAndRead(item) + + assertThat(fromParcel.settingId).isEqualTo(item.settingId) + assertThat(fromParcel.packageName).isEqualTo(item.packageName) + assertThat(fromParcel.className).isEqualTo(item.className) + assertThat(fromParcel.intentAction).isEqualTo(item.intentAction) + assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1")) + } + + private fun writeAndRead(item: DeviceSettingItem): DeviceSettingItem { + val parcel = Parcel.obtain() + item.writeToParcel(parcel, 0) + parcel.setDataPosition(0) + return DeviceSettingItem.CREATOR.createFromParcel(parcel) + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingStateTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingStateTest.java new file mode 100644 index 000000000000..12b7a0f0167c --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingStateTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.os.Bundle; +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class DeviceSettingStateTest { + private static final ActionSwitchPreferenceState ACTION_SWITCH_PREFERENCE_STATE = + new ActionSwitchPreferenceState.Builder().setChecked(true).build(); + private static final MultiTogglePreferenceState MULTI_TOGGLE_PREFERENCE_STATE = + new MultiTogglePreferenceState.Builder().setState(123).build(); + + @Test + public void build_withoutPreferenceState_fail() { + assertThrows( + IllegalArgumentException.class, + () -> { + DeviceSettingState unused = + new DeviceSettingState.Builder() + .setSettingId(123) + .setExtras(buildBundle("key1", "value1")) + .build(); + }); + } + + @Test + public void build_withoutExtra_successfully() { + DeviceSettingState unused = + new DeviceSettingState.Builder() + .setSettingId(123) + .setPreferenceState(ACTION_SWITCH_PREFERENCE_STATE) + .build(); + } + + @Test + public void build_withAllFields_successfully() { + DeviceSettingState unused = + new DeviceSettingState.Builder() + .setSettingId(123) + .setPreferenceState(ACTION_SWITCH_PREFERENCE_STATE) + .setExtras(buildBundle("key1", "value1")) + .build(); + } + + @Test + public void getMethods_actionSwitchPreferenceState() { + DeviceSettingState state = + new DeviceSettingState.Builder() + .setSettingId(123) + .setPreferenceState(ACTION_SWITCH_PREFERENCE_STATE) + .setExtras(buildBundle("key1", "value1")) + .build(); + + assertThat(state.getSettingId()).isEqualTo(123); + assertThat(state.getPreferenceState()).isInstanceOf(ActionSwitchPreferenceState.class); + assertThat(state.getExtras().getString("key1")).isEqualTo("value1"); + } + + @Test + public void getMethods_multiTogglePreference() { + DeviceSettingState state = + new DeviceSettingState.Builder() + .setSettingId(123) + .setPreferenceState(MULTI_TOGGLE_PREFERENCE_STATE) + .setExtras(buildBundle("key1", "value1")) + .build(); + + assertThat(state.getSettingId()).isEqualTo(123); + assertThat(state.getPreferenceState()).isInstanceOf(MultiTogglePreferenceState.class); + assertThat(state.getExtras().getString("key1")).isEqualTo("value1"); + } + + @Test + public void parcelOperation_actionSwitchPreferenceState() { + DeviceSettingState state = + new DeviceSettingState.Builder() + .setSettingId(123) + .setPreferenceState(ACTION_SWITCH_PREFERENCE_STATE) + .setExtras(buildBundle("key1", "value1")) + .build(); + + DeviceSettingState fromParcel = writeAndRead(state); + + assertThat(fromParcel.getSettingId()).isEqualTo(state.getSettingId()); + assertThat(fromParcel.getPreferenceState()).isInstanceOf(ActionSwitchPreferenceState.class); + assertThat(fromParcel.getPreferenceState().getSettingType()) + .isEqualTo(DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH); + assertThat(((ActionSwitchPreferenceState) fromParcel.getPreferenceState()).getChecked()) + .isTrue(); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(state.getExtras().getString("key1")); + } + + @Test + public void parcelOperation_multiTogglePreferenceState() { + DeviceSettingState state = + new DeviceSettingState.Builder() + .setSettingId(123) + .setPreferenceState(MULTI_TOGGLE_PREFERENCE_STATE) + .setExtras(buildBundle("key1", "value1")) + .build(); + + DeviceSettingState fromParcel = writeAndRead(state); + + assertThat(fromParcel.getSettingId()).isEqualTo(state.getSettingId()); + assertThat(fromParcel.getPreferenceState()).isInstanceOf(MultiTogglePreferenceState.class); + assertThat(fromParcel.getPreferenceState().getSettingType()) + .isEqualTo(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE); + assertThat(((MultiTogglePreferenceState) fromParcel.getPreferenceState()).getState()) + .isEqualTo(123); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(state.getExtras().getString("key1")); + } + + @Test + public void parcelOperation_unknownPreferenceState() { + DeviceSettingState state = + new DeviceSettingState.Builder() + .setSettingId(123) + .setPreferenceState(new DeviceSettingPreferenceState(123) {}) + .setExtras(buildBundle("key1", "value1")) + .build(); + + DeviceSettingState fromParcel = writeAndRead(state); + + assertThat(fromParcel.getSettingId()).isEqualTo(state.getSettingId()); + assertThat(fromParcel.getPreferenceState()) + .isSameInstanceAs(DeviceSettingPreferenceState.UNKNOWN); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(state.getExtras().getString("key1")); + } + + private Bundle buildBundle(String key, String value) { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + return bundle; + } + + private DeviceSettingState writeAndRead(DeviceSettingState state) { + Parcel parcel = Parcel.obtain(); + state.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DeviceSettingState fromParcel = DeviceSettingState.CREATOR.createFromParcel(parcel); + return fromParcel; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingTest.java new file mode 100644 index 000000000000..98dc54b2e33d --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.os.Bundle; +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class DeviceSettingTest { + private static final ActionSwitchPreference ACTION_SWITCH_PREFERENCE = + new ActionSwitchPreference.Builder().setTitle("action_switch_preference").build(); + private static final MultiTogglePreference MULTI_TOGGLE_PREFERENCE = + new MultiTogglePreference.Builder().setTitle("multi_toggle_preference").build(); + + @Test + public void build_withoutPreference_fail() { + assertThrows( + IllegalArgumentException.class, + () -> { + DeviceSetting unused = + new DeviceSetting.Builder() + .setSettingId(123) + .setExtras(buildBundle("key1", "value1")) + .build(); + }); + } + + @Test + public void build_withoutExtra_successfully() { + DeviceSetting unused = + new DeviceSetting.Builder() + .setSettingId(123) + .setPreference(ACTION_SWITCH_PREFERENCE) + .build(); + } + + @Test + public void build_withAllFields_successfully() { + DeviceSetting unused = + new DeviceSetting.Builder() + .setSettingId(123) + .setPreference(ACTION_SWITCH_PREFERENCE) + .setExtras(buildBundle("key1", "value1")) + .build(); + } + + @Test + public void getMethods_actionSwitchPreference() { + DeviceSetting setting = + new DeviceSetting.Builder() + .setSettingId(123) + .setPreference(ACTION_SWITCH_PREFERENCE) + .setExtras(buildBundle("key1", "value1")) + .build(); + + assertThat(setting.getSettingId()).isEqualTo(123); + assertThat(setting.getPreference()).isInstanceOf(ActionSwitchPreference.class); + assertThat(setting.getExtras().getString("key1")).isEqualTo("value1"); + } + + @Test + public void getMethods_multiTogglePreference() { + DeviceSetting setting = + new DeviceSetting.Builder() + .setSettingId(123) + .setPreference(MULTI_TOGGLE_PREFERENCE) + .setExtras(buildBundle("key1", "value1")) + .build(); + + assertThat(setting.getSettingId()).isEqualTo(123); + assertThat(setting.getPreference()).isInstanceOf(MultiTogglePreference.class); + assertThat(setting.getExtras().getString("key1")).isEqualTo("value1"); + } + + @Test + public void parcelOperation_actionSwitchPreference() { + DeviceSetting setting = + new DeviceSetting.Builder() + .setSettingId(123) + .setPreference(ACTION_SWITCH_PREFERENCE) + .setExtras(buildBundle("key1", "value1")) + .build(); + + DeviceSetting fromParcel = writeAndRead(setting); + + assertThat(fromParcel.getSettingId()).isEqualTo(setting.getSettingId()); + assertThat(fromParcel.getPreference()).isInstanceOf(ActionSwitchPreference.class); + assertThat(fromParcel.getPreference().getSettingType()) + .isEqualTo(DeviceSettingType.DEVICE_SETTING_TYPE_ACTION_SWITCH); + assertThat(((ActionSwitchPreference) fromParcel.getPreference()).getTitle()) + .isEqualTo("action_switch_preference"); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(setting.getExtras().getString("key1")); + } + + @Test + public void parcelOperation_multiTogglePreference() { + DeviceSetting setting = + new DeviceSetting.Builder() + .setSettingId(123) + .setPreference(MULTI_TOGGLE_PREFERENCE) + .setExtras(buildBundle("key1", "value1")) + .build(); + + DeviceSetting fromParcel = writeAndRead(setting); + + assertThat(fromParcel.getSettingId()).isEqualTo(setting.getSettingId()); + assertThat(fromParcel.getPreference()).isInstanceOf(MultiTogglePreference.class); + assertThat(fromParcel.getPreference().getSettingType()) + .isEqualTo(DeviceSettingType.DEVICE_SETTING_TYPE_MULTI_TOGGLE); + assertThat(((MultiTogglePreference) fromParcel.getPreference()).getTitle()) + .isEqualTo("multi_toggle_preference"); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(setting.getExtras().getString("key1")); + } + + @Test + public void parcelOperation_unknownPreference() { + DeviceSetting setting = + new DeviceSetting.Builder() + .setSettingId(123) + .setPreference(new DeviceSettingPreference(123) {}) + .setExtras(buildBundle("key1", "value1")) + .build(); + + DeviceSetting fromParcel = writeAndRead(setting); + + assertThat(fromParcel.getSettingId()).isEqualTo(setting.getSettingId()); + assertThat(fromParcel.getPreference()).isSameInstanceAs(DeviceSettingPreference.UNKNOWN); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(setting.getExtras().getString("key1")); + } + + private Bundle buildBundle(String key, String value) { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + return bundle; + } + + private DeviceSetting writeAndRead(DeviceSetting state) { + Parcel parcel = Parcel.obtain(); + state.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + DeviceSetting fromParcel = DeviceSetting.CREATOR.createFromParcel(parcel); + return fromParcel; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt new file mode 100644 index 000000000000..2b29a6ef5e5a --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings + +import android.os.Bundle +import android.os.Parcel +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DeviceSettingsConfigTest { + + @Test + fun parcelOperation() { + val config = + DeviceSettingsConfig( + mainContentItems = + listOf( + DeviceSettingItem( + 1, + "package_name_1", + "class_name_1", + "intent_action_1", + Bundle() + ) + ), + moreSettingsItems = + listOf( + DeviceSettingItem( + 2, + "package_name_2", + "class_name_2", + "intent_action_2", + Bundle() + ) + ), + moreSettingsFooter = "footer", + extras = Bundle().apply { putString("key1", "value1") }, + ) + + val fromParcel = writeAndRead(config) + + assertThat(fromParcel.mainContentItems.stream().map { it.settingId }.toList()) + .containsExactly(1) + assertThat(fromParcel.mainContentItems.stream().map { it.packageName }.toList()) + .containsExactly("package_name_1") + assertThat(fromParcel.mainContentItems.stream().map { it.className }.toList()) + .containsExactly("class_name_1") + assertThat(fromParcel.mainContentItems.stream().map { it.intentAction }.toList()) + .containsExactly("intent_action_1") + assertThat(fromParcel.moreSettingsItems.stream().map { it.settingId }.toList()) + .containsExactly(2) + assertThat(fromParcel.moreSettingsItems.stream().map { it.packageName }.toList()) + .containsExactly("package_name_2") + assertThat(fromParcel.moreSettingsItems.stream().map { it.className }.toList()) + .containsExactly("class_name_2") + assertThat(fromParcel.moreSettingsItems.stream().map { it.intentAction }.toList()) + .containsExactly("intent_action_2") + assertThat(fromParcel.moreSettingsFooter).isEqualTo(config.moreSettingsFooter) + } + + private fun writeAndRead(item: DeviceSettingsConfig): DeviceSettingsConfig { + val parcel = Parcel.obtain() + item.writeToParcel(parcel, 0) + parcel.setDataPosition(0) + return DeviceSettingsConfig.CREATOR.createFromParcel(parcel) + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceStateTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceStateTest.java new file mode 100644 index 000000000000..2645fc5f16ff --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceStateTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Bundle; +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class MultiTogglePreferenceStateTest { + + @Test + public void getMethods() { + MultiTogglePreferenceState state1 = + new MultiTogglePreferenceState.Builder() + .setState(1) + .setExtras(buildBundle("key1", "value1")) + .build(); + MultiTogglePreferenceState state2 = + new MultiTogglePreferenceState.Builder() + .setState(2) + .setExtras(buildBundle("key2", "value2")) + .build(); + + assertThat(state1.getState()).isEqualTo(1); + assertThat(state2.getState()).isEqualTo(2); + assertThat(state1.getExtras().getString("key1")).isEqualTo("value1"); + assertThat(state2.getExtras().getString("key2")).isEqualTo("value2"); + } + + @Test + public void parcelOperation() { + MultiTogglePreferenceState state = + new MultiTogglePreferenceState.Builder() + .setState(123) + .setExtras(buildBundle("key1", "value1")) + .build(); + + MultiTogglePreferenceState fromParcel = writeAndRead(state); + + assertThat(fromParcel.getState()).isEqualTo(state.getState()); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(state.getExtras().getString("key1")); + } + + private Bundle buildBundle(String key, String value) { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + return bundle; + } + + private MultiTogglePreferenceState writeAndRead(MultiTogglePreferenceState state) { + Parcel parcel = Parcel.obtain(); + state.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + MultiTogglePreferenceState fromParcel = + MultiTogglePreferenceState.CREATOR.createFromParcel(parcel); + return fromParcel; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java new file mode 100644 index 000000000000..62fcb5ec3011 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/MultiTogglePreferenceTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class MultiTogglePreferenceTest { + private static final Bitmap ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + private static final ToggleInfo TOGGLE_INFO_1 = + new ToggleInfo.Builder().setLabel("label1").setIcon(ICON).build(); + private static final ToggleInfo TOGGLE_INFO_2 = + new ToggleInfo.Builder().setLabel("label2").setIcon(ICON).build(); + + @Test + public void build_withoutTitle_fail() { + assertThrows( + IllegalArgumentException.class, + () -> { + MultiTogglePreference unused = + new MultiTogglePreference.Builder() + .addToggleInfo(TOGGLE_INFO_1) + .setState(0) + .setAllowChangingState(true) + .setExtras(buildBundle("key1", "value1")) + .build(); + }); + } + + @Test + public void build_withNegativeState_fail() { + assertThrows( + IllegalArgumentException.class, + () -> { + MultiTogglePreference unused = + new MultiTogglePreference.Builder() + .setTitle("title") + .addToggleInfo(TOGGLE_INFO_1) + .setState(-1) + .setAllowChangingState(true) + .setExtras(buildBundle("key1", "value1")) + .build(); + }); + } + + @Test + public void build_withoutExtra_successfully() { + MultiTogglePreference unused = + new MultiTogglePreference.Builder() + .setTitle("title") + .addToggleInfo(TOGGLE_INFO_1) + .addToggleInfo(TOGGLE_INFO_2) + .setState(123) + .setAllowChangingState(true) + .build(); + } + + @Test + public void build_withAllFields_successfully() { + MultiTogglePreference unused = + new MultiTogglePreference.Builder() + .setTitle("title") + .addToggleInfo(TOGGLE_INFO_1) + .addToggleInfo(TOGGLE_INFO_2) + .setState(123) + .setAllowChangingState(true) + .setExtras(buildBundle("key1", "value1")) + .build(); + } + + @Test + public void getMethods() { + MultiTogglePreference preference = + new MultiTogglePreference.Builder() + .setTitle("title") + .addToggleInfo(TOGGLE_INFO_1) + .addToggleInfo(TOGGLE_INFO_2) + .setState(123) + .setAllowChangingState(true) + .setExtras(buildBundle("key1", "value1")) + .build(); + + assertThat(preference.getTitle()).isEqualTo("title"); + assertThat(preference.getToggleInfos().stream().map(ToggleInfo::getLabel).toList()) + .containsExactly("label1", "label2"); + assertThat(preference.getState()).isEqualTo(123); + assertThat(preference.isAllowedChangingState()).isTrue(); + assertThat(preference.getExtras().getString("key1")).isEqualTo("value1"); + } + + @Test + public void parcelOperation() { + MultiTogglePreference preference = + new MultiTogglePreference.Builder() + .setTitle("title") + .addToggleInfo(TOGGLE_INFO_1) + .addToggleInfo(TOGGLE_INFO_2) + .setState(123) + .setAllowChangingState(true) + .setExtras(buildBundle("key1", "value1")) + .build(); + + MultiTogglePreference fromParcel = writeAndRead(preference); + + assertThat(fromParcel.getTitle()).isEqualTo(preference.getTitle()); + assertThat(fromParcel.getToggleInfos().stream().map(ToggleInfo::getLabel).toList()) + .containsExactly("label1", "label2"); + assertThat(fromParcel.getState()).isEqualTo(preference.getState()); + assertThat(fromParcel.isAllowedChangingState()) + .isEqualTo(preference.isAllowedChangingState()); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(preference.getExtras().getString("key1")); + } + + private Bundle buildBundle(String key, String value) { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + return bundle; + } + + private MultiTogglePreference writeAndRead(MultiTogglePreference preference) { + Parcel parcel = Parcel.obtain(); + preference.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + MultiTogglePreference fromParcel = MultiTogglePreference.CREATOR.createFromParcel(parcel); + return fromParcel; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfoTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfoTest.java new file mode 100644 index 000000000000..439749a53d32 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/ToggleInfoTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 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.bluetooth.devicesettings; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcel; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public final class ToggleInfoTest { + + @Test + public void build_withoutIcon_fail() { + assertThrows( + IllegalArgumentException.class, + () -> { + ToggleInfo unused = + new ToggleInfo.Builder() + .setLabel("label") + .setExtras(buildBundle("key1", "value1")) + .build(); + }); + } + + @Test + public void build_withoutLabel_fail() { + assertThrows( + IllegalArgumentException.class, + () -> { + ToggleInfo unused = + new ToggleInfo.Builder() + .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + .setExtras(buildBundle("key1", "value1")) + .build(); + }); + } + + @Test + public void build_withoutExtra_successfully() { + ToggleInfo unused = + new ToggleInfo.Builder() + .setLabel("label") + .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + .build(); + } + + @Test + public void build_withAllFields_successfully() { + ToggleInfo unused = + new ToggleInfo.Builder() + .setLabel("label") + .setIcon(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)) + .setExtras(buildBundle("key1", "value1")) + .build(); + } + + @Test + public void getMethods() { + Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + ToggleInfo info = + new ToggleInfo.Builder() + .setLabel("label") + .setIcon(icon) + .setExtras(buildBundle("key1", "value1")) + .build(); + + assertThat(info.getLabel()).isEqualTo("label"); + assertThat(info.getIcon()).isSameInstanceAs(icon); + assertThat(info.getExtras().getString("key1")).isEqualTo("value1"); + } + + @Test + public void parcelOperation() { + Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + ToggleInfo info = + new ToggleInfo.Builder() + .setLabel("label") + .setIcon(icon) + .setExtras(buildBundle("key1", "value1")) + .build(); + + ToggleInfo fromParcel = writeAndRead(info); + + assertThat(fromParcel.getLabel()).isEqualTo(info.getLabel()); + assertThat(fromParcel.getIcon().sameAs(info.getIcon())).isTrue(); + assertThat(fromParcel.getExtras().getString("key1")) + .isEqualTo(info.getExtras().getString("key1")); + } + + private Bundle buildBundle(String key, String value) { + Bundle bundle = new Bundle(); + bundle.putString(key, value); + return bundle; + } + + private ToggleInfo writeAndRead(ToggleInfo state) { + Parcel parcel = Parcel.obtain(); + state.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ToggleInfo fromParcel = ToggleInfo.CREATOR.createFromParcel(parcel); + return fromParcel; + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java index c9b35a0ae833..e1447dc8410c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java @@ -861,6 +861,23 @@ public class InfoMediaManagerTest { } @Test + public void addMediaDevice_withAddresslessBluetoothDevice_shouldIgnoreDeviceAndNotCrash() { + MediaRoute2Info bluetoothRoute = + new MediaRoute2Info.Builder(TEST_BLUETOOTH_ROUTE).setAddress(null).build(); + + final CachedBluetoothDeviceManager cachedBluetoothDeviceManager = + mock(CachedBluetoothDeviceManager.class); + when(mLocalBluetoothManager.getCachedDeviceManager()) + .thenReturn(cachedBluetoothDeviceManager); + when(cachedBluetoothDeviceManager.findDevice(any(BluetoothDevice.class))).thenReturn(null); + + mInfoMediaManager.mMediaDevices.clear(); + mInfoMediaManager.addMediaDevice(bluetoothRoute, TEST_SYSTEM_ROUTING_SESSION); + + assertThat(mInfoMediaManager.mMediaDevices.size()).isEqualTo(0); + } + + @Test public void onRoutesUpdated_setsFirstSelectedRouteAsCurrentConnectedDevice() { final CachedBluetoothDeviceManager cachedBluetoothDeviceManager = mock(CachedBluetoothDeviceManager.class); diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 07f74367fd06..3f165a3bcb63 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1052,6 +1052,16 @@ flag { } flag { + name: "glanceable_hub_back_gesture" + namespace: "systemui" + description: "Enables back gesture on the glanceable hub" + bug: "346331399" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "glanceable_hub_allow_keyguard_when_dreaming" namespace: "systemui" description: "Allows users to exit dream to keyguard with glanceable hub enabled" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index cc4e7752ab21..d0466313cf81 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.CommunalSwipeDetector import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey @@ -41,6 +42,7 @@ import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.observableTransitionState import com.android.compose.animation.scene.transitions import com.android.compose.theme.LocalAndroidColorScheme +import com.android.systemui.Flags.glanceableHubBackGesture import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalTransitionKeys @@ -193,10 +195,17 @@ fun CommunalContainer( Box(modifier = Modifier.fillMaxSize()) } - scene( - CommunalScenes.Communal, - userActions = mapOf(Swipe(SwipeDirection.Right) to CommunalScenes.Blank) - ) { + val userActions = + if (glanceableHubBackGesture()) { + mapOf( + Swipe(SwipeDirection.Right) to CommunalScenes.Blank, + Back to CommunalScenes.Blank, + ) + } else { + mapOf(Swipe(SwipeDirection.Right) to CommunalScenes.Blank) + } + + scene(CommunalScenes.Communal, userActions = userActions) { CommunalScene( backgroundType = backgroundType, colors = colors, diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index a614fc118636..4ef1f93481f7 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -126,6 +126,8 @@ public class QuickStepContract { public static final long SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED = 1L << 33; // PiP animation is running public static final long SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING = 1L << 34; + // Communal hub is showing + public static final long SYSUI_STATE_COMMUNAL_HUB_SHOWING = 1L << 35; // Mask for SystemUiStateFlags to isolate SYSUI_STATE_AWAKE and // SYSUI_STATE_WAKEFULNESS_TRANSITION, to match WAKEFULNESS_* constants @@ -176,6 +178,7 @@ public class QuickStepContract { SYSUI_STATE_SHORTCUT_HELPER_SHOWING, SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED, SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING, + SYSUI_STATE_COMMUNAL_HUB_SHOWING, }) public @interface SystemUiStateFlags {} @@ -283,6 +286,9 @@ public class QuickStepContract { if ((flags & SYSUI_STATE_DISABLE_GESTURE_PIP_ANIMATING) != 0) { str.add("disable_gesture_pip_animating"); } + if ((flags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { + str.add("communal_hub_showing"); + } return str.toString(); } @@ -336,7 +342,8 @@ public class QuickStepContract { // the keyguard) if ((sysuiStateFlags & SYSUI_STATE_BOUNCER_SHOWING) != 0 || (sysuiStateFlags & SYSUI_STATE_DIALOG_SHOWING) != 0 - || (sysuiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0) { + || (sysuiStateFlags & SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING) != 0 + || (sysuiStateFlags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { return false; } if ((sysuiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0) { diff --git a/packages/SystemUI/src-debug/com/android/systemui/log/DebugLogger.kt b/packages/SystemUI/src-debug/com/android/systemui/log/DebugLogger.kt index af29b05a3fb1..8d1de0e65da5 100644 --- a/packages/SystemUI/src-debug/com/android/systemui/log/DebugLogger.kt +++ b/packages/SystemUI/src-debug/com/android/systemui/log/DebugLogger.kt @@ -62,6 +62,8 @@ object DebugLogger { * @param error: a [Throwable] to log. * @param message: a lazily evaluated message you wish to log. */ + @JvmOverloads + @JvmName("logcatMessage") inline fun Any.debugLog( enabled: Boolean = Build.IS_DEBUGGABLE, priority: Int = Log.DEBUG, diff --git a/packages/SystemUI/src-release/com/android/systemui/log/DebugLogger.kt b/packages/SystemUI/src-release/com/android/systemui/log/DebugLogger.kt index 2764a1fdfe3d..e29ce2da9970 100644 --- a/packages/SystemUI/src-release/com/android/systemui/log/DebugLogger.kt +++ b/packages/SystemUI/src-release/com/android/systemui/log/DebugLogger.kt @@ -22,6 +22,7 @@ import android.util.Log /** An empty logger for release builds. */ object DebugLogger { + @JvmOverloads @JvmName("logcatMessage") inline fun Any.debugLog( enabled: Boolean = Build.IS_DEBUGGABLE, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index 37e9dc1a9d48..7750f6bf4178 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -233,7 +233,7 @@ public class SystemActions implements CoreStartable, ConfigurationController.Con // NotificationShadeWindowController.registerCallback() only keeps weak references. mNotificationShadeCallback = (keyguardShowing, keyguardOccluded, keyguardGoingAway, bouncerShowing, mDozing, - panelExpanded, isDreaming) -> + panelExpanded, isDreaming, communalShowing) -> registerOrUnregisterDismissNotificationShadeAction(); mScreenshotHelper = new ScreenshotHelper(mContext); } diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt index 5084944685b6..42f66cca2522 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt @@ -18,12 +18,14 @@ package com.android.systemui.model import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey +import com.android.systemui.Flags.glanceableHubBackGesture import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED @@ -105,6 +107,10 @@ constructor( { it.scene == Scenes.Lockscreen && it.invisibleDueToOcclusion }, + SYSUI_STATE_COMMUNAL_HUB_SHOWING to + { + glanceableHubBackGesture() && it.scene == Scenes.Communal + } ) } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 1780b828cfa1..b3624ad4ad82 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -26,11 +26,13 @@ import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; +import static com.android.systemui.Flags.glanceableHubBackGesture; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_SYSUI_PROXY; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_UNLOCK_ANIMATION_CONTROLLER; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_AWAKE; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE; @@ -792,7 +794,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private void onStatusBarStateChanged(boolean keyguardShowing, boolean keyguardOccluded, boolean keyguardGoingAway, boolean bouncerShowing, boolean isDozing, - boolean panelExpanded, boolean isDreaming) { + boolean panelExpanded, boolean isDreaming, boolean communalShowing) { mSysUiState.setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING, keyguardShowing && !keyguardOccluded) .setFlag(SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED, @@ -802,6 +804,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis .setFlag(SYSUI_STATE_BOUNCER_SHOWING, bouncerShowing) .setFlag(SYSUI_STATE_DEVICE_DOZING, isDozing) .setFlag(SYSUI_STATE_DEVICE_DREAMING, isDreaming) + .setFlag(SYSUI_STATE_COMMUNAL_HUB_SHOWING, + glanceableHubBackGesture() && communalShowing) .commitUpdate(mContext.getDisplayId()); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java index c4f6cd9aac29..8feefa4eca0f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java @@ -62,6 +62,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLogger.UiEventEnum; import com.android.settingslib.Utils; import com.android.systemui.Flags; +import com.android.systemui.log.DebugLogger; import com.android.systemui.res.R; import com.android.systemui.screenshot.scroll.CropView; import com.android.systemui.settings.UserTracker; @@ -307,13 +308,16 @@ public class AppClipsActivity extends ComponentActivity { && mViewModel.getBacklinksLiveData().getValue() != null) { ClipData backlinksData = mViewModel.getBacklinksLiveData().getValue().getClipData(); data.putParcelable(EXTRA_CLIP_DATA, backlinksData); + + DebugLogger.INSTANCE.logcatMessage(this, + () -> "setResultThenFinish: sending notes app ClipData"); } try { mResultReceiver.send(Activity.RESULT_OK, data); logUiEvent(SCREENSHOT_FOR_NOTE_ACCEPTED); } catch (Exception e) { - Log.e(TAG, "Error while returning data to trampoline activity", e); + Log.e(TAG, "Error while sending data to trampoline activity", e); } // Nullify the ResultReceiver before finishing to avoid resending the result. @@ -354,6 +358,7 @@ public class AppClipsActivity extends ComponentActivity { } } catch (Exception e) { // Do nothing. + Log.e(TAG, "Error while sending trampoline activity error code: " + errorCode, e); } // Nullify the ResultReceiver to avoid resending the result. diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java index 0161f787b459..ef18fbe6db8b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java @@ -53,6 +53,7 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.log.DebugLogger; import com.android.systemui.notetask.NoteTaskController; import com.android.systemui.notetask.NoteTaskEntryPoint; import com.android.systemui.res.R; @@ -265,11 +266,15 @@ public class AppClipsTrampolineActivity extends Activity { if (statusCode == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) { Uri uri = resultData.getParcelable(EXTRA_SCREENSHOT_URI, Uri.class); convertedData.setData(uri); - } - if (resultData.containsKey(EXTRA_CLIP_DATA)) { - ClipData backlinksData = resultData.getParcelable(EXTRA_CLIP_DATA, ClipData.class); - convertedData.setClipData(backlinksData); + if (resultData.containsKey(EXTRA_CLIP_DATA)) { + ClipData backlinksData = resultData.getParcelable(EXTRA_CLIP_DATA, + ClipData.class); + convertedData.setClipData(backlinksData); + + DebugLogger.INSTANCE.logcatMessage(this, + () -> "onReceiveResult: sending notes app ClipData"); + } } // Broadcast no longer required, setting it to null. diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java index d30d518f5777..8c833eccb1fb 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -54,6 +54,7 @@ import androidx.lifecycle.ViewModelProvider; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.log.DebugLogger; import com.android.systemui.screenshot.AssistContentRequester; import com.android.systemui.screenshot.ImageExporter; @@ -143,6 +144,7 @@ final class AppClipsViewModel extends ViewModel { * @param displayId id of the display to query tasks for Backlinks data */ void triggerBacklinks(Set<Integer> taskIdsToIgnore, int displayId) { + DebugLogger.INSTANCE.logcatMessage(this, () -> "Backlinks triggered"); mBgExecutor.execute(() -> { ListenableFuture<InternalBacklinksData> backlinksData = getBacklinksData( taskIdsToIgnore, displayId); @@ -247,6 +249,10 @@ final class AppClipsViewModel extends ViewModel { } private boolean shouldIncludeTask(RootTaskInfo taskInfo, Set<Integer> taskIdsToIgnore) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> String.format("shouldIncludeTask taskId %d; topActivity %s", taskInfo.taskId, + taskInfo.topActivity)); + // Only consider tasks that shouldn't be ignored, are visible, running, and have a launcher // icon. Furthermore, types such as launcher/home/dock/assistant are ignored. return !taskIdsToIgnore.contains(taskInfo.taskId) @@ -267,6 +273,10 @@ final class AppClipsViewModel extends ViewModel { private ListenableFuture<InternalBacklinksData> getBacklinksDataForTaskId( RootTaskInfo taskInfo) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> String.format("getBacklinksDataForTaskId for taskId %d; topActivity %s", + taskInfo.taskId, taskInfo.topActivity)); + SettableFuture<InternalBacklinksData> backlinksData = SettableFuture.create(); int taskId = taskInfo.taskId; mAssistContentRequester.requestAssistContent(taskId, assistContent -> @@ -295,6 +305,10 @@ final class AppClipsViewModel extends ViewModel { */ private InternalBacklinksData getBacklinksDataFromAssistContent(RootTaskInfo taskInfo, @Nullable AssistContent content) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> String.format("getBacklinksDataFromAssistContent taskId %d; topActivity %s", + taskInfo.taskId, taskInfo.topActivity)); + String appName = getAppNameOfTask(taskInfo); String packageName = taskInfo.topActivity.getPackageName(); Drawable appIcon = taskInfo.topActivityInfo.loadIcon(mPackageManager); @@ -307,22 +321,34 @@ final class AppClipsViewModel extends ViewModel { // First preference is given to app provided uri. if (content.isAppProvidedWebUri()) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> "getBacklinksDataFromAssistContent: app has provided a uri"); + Uri uri = content.getWebUri(); Intent backlinksIntent = new Intent(ACTION_VIEW).setData(uri); if (doesIntentResolveToSamePackage(backlinksIntent, packageName)) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> "getBacklinksDataFromAssistContent: using app provided uri"); return new InternalBacklinksData(ClipData.newRawUri(appName, uri), appIcon); } } // Second preference is given to app provided, hopefully deep-linking, intent. if (content.isAppProvidedIntent()) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> "getBacklinksDataFromAssistContent: app has provided an intent"); + Intent backlinksIntent = content.getIntent(); if (doesIntentResolveToSamePackage(backlinksIntent, packageName)) { + DebugLogger.INSTANCE.logcatMessage(this, + () -> "getBacklinksDataFromAssistContent: using app provided intent"); return new InternalBacklinksData(ClipData.newIntent(appName, backlinksIntent), appIcon); } } + DebugLogger.INSTANCE.logcatMessage(this, + () -> "getBacklinksDataFromAssistContent: using fallback"); return fallback; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index ba4c29aad6a2..d870fe69463d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade import android.content.Context +import android.graphics.Insets import android.graphics.Rect import android.os.PowerManager import android.os.SystemClock @@ -25,6 +26,7 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.view.WindowInsets import android.widget.FrameLayout import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner @@ -37,6 +39,7 @@ import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.Flags.glanceableHubBackGesture import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.communal.dagger.Communal @@ -259,15 +262,33 @@ constructor( // Run when the touch handling lifecycle is RESUMED, meaning the hub is visible and not // occluded. lifecycleRegistry.repeatOnLifecycle(Lifecycle.State.RESUMED) { - val exclusionRect = - Rect( - 0, - topEdgeSwipeRegionWidth, - containerView.right, - containerView.bottom - bottomEdgeSwipeRegionWidth - ) + // Avoid adding exclusion to right/left edges to allow back gestures. + val insets = + if (glanceableHubBackGesture()) { + containerView.rootWindowInsets.getInsets(WindowInsets.Type.systemGestures()) + } else { + Insets.NONE + } - containerView.systemGestureExclusionRects = listOf(exclusionRect) + containerView.systemGestureExclusionRects = + listOf( + // Only allow swipe up to bouncer and swipe down to shade in the very + // top/bottom to avoid conflicting with widgets in the hub grid. + Rect( + insets.left, + topEdgeSwipeRegionWidth, + containerView.right - insets.right, + containerView.bottom - bottomEdgeSwipeRegionWidth + ), + // Disable back gestures on the left side of the screen, to avoid + // conflicting with scene transitions. + Rect( + 0, + 0, + insets.right, + containerView.bottom, + ) + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index f67d0c185e0f..bc5cf2a87925 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -644,7 +644,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mCurrentState.bouncerShowing, mCurrentState.dozing, mCurrentState.shadeOrQsExpanded, - mCurrentState.dreaming); + mCurrentState.dreaming, + mCurrentState.communalVisible); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java index da91d6a05d6b..6ac7f11d3ad8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowCallback.java @@ -26,5 +26,5 @@ public interface StatusBarWindowCallback { */ void onStateChanged(boolean keyguardShowing, boolean keyguardOccluded, boolean keyguardGoingAway, boolean bouncerShowing, boolean isDozing, - boolean panelExpanded, boolean isDreaming); + boolean panelExpanded, boolean isDreaming, boolean communalShowing); } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java index 8457bdb2d0ff..45799b255630 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java @@ -261,7 +261,7 @@ public class BubblesManager { // Store callback in a field so it won't get GC'd mStatusBarWindowCallback = (keyguardShowing, keyguardOccluded, keyguardGoingAway, bouncerShowing, isDozing, - panelExpanded, isDreaming) -> { + panelExpanded, isDreaming, communalShowing) -> { if (panelExpanded != mPanelExpanded) { mPanelExpanded = panelExpanded; mBubbles.onNotificationPanelExpandedChanged(panelExpanded); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index b0213a4b6421..169511f827ef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -16,19 +16,24 @@ package com.android.systemui.shade +import android.graphics.Insets import android.graphics.Rect import android.os.PowerManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.ViewUtils import android.view.MotionEvent import android.view.View +import android.view.WindowInsets import android.widget.FrameLayout import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.test.filters.SmallTest import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_BACK_GESTURE import com.android.systemui.SysuiTestCase import com.android.systemui.ambient.touch.TouchHandler import com.android.systemui.ambient.touch.TouchMonitor @@ -66,6 +71,9 @@ import org.mockito.ArgumentMatchers.anyFloat import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy @ExperimentalCoroutinesApi @RunWith(AndroidTestingRunner::class) @@ -317,6 +325,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE) fun gestureExclusionZone_setAfterInit() = with(kosmos) { testScope.runTest { @@ -325,10 +334,41 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { assertThat(containerView.systemGestureExclusionRects) .containsExactly( Rect( - /* left */ 0, - /* top */ TOP_SWIPE_REGION_WIDTH, - /* right */ CONTAINER_WIDTH, - /* bottom */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH + /* left= */ 0, + /* top= */ TOP_SWIPE_REGION_WIDTH, + /* right= */ CONTAINER_WIDTH, + /* bottom= */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH + ), + Rect( + /* left= */ 0, + /* top= */ 0, + /* right= */ 0, + /* bottom= */ CONTAINER_HEIGHT + ) + ) + } + } + + @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_BACK_GESTURE) + fun gestureExclusionZone_setAfterInit_backGestureEnabled() = + with(kosmos) { + testScope.runTest { + goToScene(CommunalScenes.Communal) + + assertThat(containerView.systemGestureExclusionRects) + .containsExactly( + Rect( + /* left= */ FAKE_INSETS.left, + /* top= */ TOP_SWIPE_REGION_WIDTH, + /* right= */ CONTAINER_WIDTH - FAKE_INSETS.right, + /* bottom= */ CONTAINER_HEIGHT - BOTTOM_SWIPE_REGION_WIDTH + ), + Rect( + /* left= */ 0, + /* top= */ 0, + /* right= */ FAKE_INSETS.right, + /* bottom= */ CONTAINER_HEIGHT ) ) } @@ -340,6 +380,9 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { testScope.runTest { goToScene(CommunalScenes.Communal) + // Exclusion rect is set. + assertThat(containerView.systemGestureExclusionRects).isNotEmpty() + // Shade shows up. shadeTestUtil.setQsExpansion(1.0f) testableLooper.processAllMessages() @@ -355,6 +398,9 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { testScope.runTest { goToScene(CommunalScenes.Communal) + // Exclusion rect is set. + assertThat(containerView.systemGestureExclusionRects).isNotEmpty() + // Bouncer is visible. fakeKeyguardBouncerRepository.setPrimaryShow(true) testableLooper.processAllMessages() @@ -371,7 +417,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { goToScene(CommunalScenes.Communal) // Exclusion rect is set. - assertThat(containerView.systemGestureExclusionRects).hasSize(1) + assertThat(containerView.systemGestureExclusionRects).isNotEmpty() // Leave the hub. goToScene(CommunalScenes.Blank) @@ -399,7 +445,12 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } private fun initAndAttachContainerView() { - containerView = View(context) + val mockInsets = + mock<WindowInsets> { + on { getInsets(WindowInsets.Type.systemGestures()) } doReturn FAKE_INSETS + } + + containerView = spy(View(context)) { on { rootWindowInsets } doReturn mockInsets } parentView = FrameLayout(context) @@ -422,6 +473,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { private const val RIGHT_SWIPE_REGION_WIDTH = 20 private const val TOP_SWIPE_REGION_WIDTH = 12 private const val BOTTOM_SWIPE_REGION_WIDTH = 14 + private val FAKE_INSETS = Insets.of(10, 20, 30, 50) /** * A touch down event right in the middle of the screen, to avoid being in any of the swipe diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java index 3883604b7134..bd759f378d5b 100644 --- a/services/core/java/com/android/server/display/color/ColorDisplayService.java +++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java @@ -25,6 +25,9 @@ import static android.hardware.display.ColorDisplayManager.COLOR_MODE_NATURAL; import static android.hardware.display.ColorDisplayManager.COLOR_MODE_SATURATED; import static android.hardware.display.ColorDisplayManager.VENDOR_COLOR_MODE_RANGE_MAX; import static android.hardware.display.ColorDisplayManager.VENDOR_COLOR_MODE_RANGE_MIN; +import static android.os.UserHandle.USER_SYSTEM; +import static android.os.UserHandle.getCallingUserId; +import static android.os.UserManager.isVisibleBackgroundUsersEnabled; import static com.android.server.display.color.DisplayTransformManager.LEVEL_COLOR_MATRIX_NIGHT_DISPLAY; @@ -80,6 +83,7 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.accessibility.Flags; import com.android.server.display.feature.DisplayManagerFlags; +import com.android.server.pm.UserManagerService; import com.android.server.twilight.TwilightListener; import com.android.server.twilight.TwilightManager; import com.android.server.twilight.TwilightState; @@ -186,9 +190,14 @@ public final class ColorDisplayService extends SystemService { private final Object mCctTintApplierLock = new Object(); + private final boolean mVisibleBackgroundUsersEnabled; + private final UserManagerService mUserManager; + public ColorDisplayService(Context context) { super(context); mHandler = new TintHandler(DisplayThread.get().getLooper()); + mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled(); + mUserManager = UserManagerService.getInstance(); } @Override @@ -1745,6 +1754,9 @@ public final class ColorDisplayService extends SystemService { @Override public void setColorMode(int colorMode) { setColorMode_enforcePermission(); + + enforceValidCallingUser("setColorMode"); + final long token = Binder.clearCallingIdentity(); try { setColorModeInternal(colorMode); @@ -1784,6 +1796,9 @@ public final class ColorDisplayService extends SystemService { if (!hasTransformsPermission && !hasLegacyPermission) { throw new SecurityException("Permission required to set display saturation level"); } + + enforceValidCallingUser("setSaturationLevel"); + final long token = Binder.clearCallingIdentity(); try { setSaturationLevelInternal(level); @@ -1812,6 +1827,8 @@ public final class ColorDisplayService extends SystemService { public boolean setAppSaturationLevel(String packageName, int level) { super.setAppSaturationLevel_enforcePermission(); + enforceValidCallingUser("setAppSaturationLevel"); + final String callingPackageName = LocalServices.getService(PackageManagerInternal.class) .getNameForUid(Binder.getCallingUid()); final long token = Binder.clearCallingIdentity(); @@ -1838,6 +1855,9 @@ public final class ColorDisplayService extends SystemService { @Override public boolean setNightDisplayActivated(boolean activated) { setNightDisplayActivated_enforcePermission(); + + enforceValidCallingUser("setNightDisplayActivated"); + final long token = Binder.clearCallingIdentity(); try { mNightDisplayTintController.setActivated(activated); @@ -1861,6 +1881,9 @@ public final class ColorDisplayService extends SystemService { @Override public boolean setNightDisplayColorTemperature(int temperature) { setNightDisplayColorTemperature_enforcePermission(); + + enforceValidCallingUser("setNightDisplayColorTemperature"); + final long token = Binder.clearCallingIdentity(); try { return mNightDisplayTintController.setColorTemperature(temperature); @@ -1883,6 +1906,9 @@ public final class ColorDisplayService extends SystemService { @Override public boolean setNightDisplayAutoMode(int autoMode) { setNightDisplayAutoMode_enforcePermission(); + + enforceValidCallingUser("setNightDisplayAutoMode"); + final long token = Binder.clearCallingIdentity(); try { return setNightDisplayAutoModeInternal(autoMode); @@ -1917,6 +1943,9 @@ public final class ColorDisplayService extends SystemService { @Override public boolean setNightDisplayCustomStartTime(Time startTime) { setNightDisplayCustomStartTime_enforcePermission(); + + enforceValidCallingUser("setNightDisplayCustomStartTime"); + final long token = Binder.clearCallingIdentity(); try { return setNightDisplayCustomStartTimeInternal(startTime); @@ -1939,6 +1968,9 @@ public final class ColorDisplayService extends SystemService { @Override public boolean setNightDisplayCustomEndTime(Time endTime) { setNightDisplayCustomEndTime_enforcePermission(); + + enforceValidCallingUser("setNightDisplayCustomEndTime"); + final long token = Binder.clearCallingIdentity(); try { return setNightDisplayCustomEndTimeInternal(endTime); @@ -1961,6 +1993,9 @@ public final class ColorDisplayService extends SystemService { @Override public boolean setDisplayWhiteBalanceEnabled(boolean enabled) { setDisplayWhiteBalanceEnabled_enforcePermission(); + + enforceValidCallingUser("setDisplayWhiteBalanceEnabled"); + final long token = Binder.clearCallingIdentity(); try { return setDisplayWhiteBalanceSettingEnabled(enabled); @@ -1993,6 +2028,9 @@ public final class ColorDisplayService extends SystemService { @Override public boolean setReduceBrightColorsActivated(boolean activated) { setReduceBrightColorsActivated_enforcePermission(); + + enforceValidCallingUser("setReduceBrightColorsActivated"); + final long token = Binder.clearCallingIdentity(); try { return setReduceBrightColorsActivatedInternal(activated); @@ -2025,6 +2063,9 @@ public final class ColorDisplayService extends SystemService { @Override public boolean setReduceBrightColorsStrength(int strength) { setReduceBrightColorsStrength_enforcePermission(); + + enforceValidCallingUser("setReduceBrightColorsStrength"); + final long token = Binder.clearCallingIdentity(); try { return setReduceBrightColorsStrengthInternal(strength); @@ -2064,4 +2105,32 @@ public final class ColorDisplayService extends SystemService { } } } + + /** + * This method validates whether the calling user is allowed to set display's color transform + * on a device that enables visible background users. + * Only system or current user or the user that belongs to the same profile group as the current + * user is permitted to set the color transform. + */ + private void enforceValidCallingUser(String method) { + if (!mVisibleBackgroundUsersEnabled) { + return; + } + + int callingUserId = getCallingUserId(); + if (callingUserId == USER_SYSTEM || callingUserId == mCurrentUser) { + return; + } + long ident = Binder.clearCallingIdentity(); + try { + if (mUserManager.isSameProfileGroup(callingUserId, mCurrentUser)) { + return; + } + } finally { + Binder.restoreCallingIdentity(ident); + } + + throw new SecurityException("Calling user id: " + callingUserId + + ", is not permitted to use Method " + method + "()."); + } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index d9adb0e05a3b..efe07dc36646 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -53,6 +53,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; +import static android.app.WindowConfiguration.isFloating; import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION; import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE; import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; @@ -342,6 +343,7 @@ import android.service.contentcapture.ActivityEvent; import android.service.dreams.DreamActivity; import android.service.voice.IVoiceInteractionSession; import android.util.ArraySet; +import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; import android.util.MergedConfiguration; @@ -2923,14 +2925,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** Makes starting window always fill the associated task. */ private void attachStartingSurfaceToAssociatedTask() { - if (mSyncState == SYNC_STATE_NONE && isEmbedded()) { - // Collect this activity since it's starting window will reparent to task. To ensure - // any starting window's transaction will occur in order. - mTransitionController.collect(this); - } + mTransitionController.collect(mStartingWindow); // Associate the configuration of starting window with the task. overrideConfigurationPropagation(mStartingWindow, mStartingData.mAssociatedTask); - getSyncTransaction().reparent(mStartingWindow.mSurfaceControl, + mStartingWindow.getSyncTransaction().reparent(mStartingWindow.mSurfaceControl, mStartingData.mAssociatedTask.mSurfaceControl); } @@ -8685,7 +8683,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); } - applySizeOverrideIfNeeded(newParentConfiguration, resolvedConfig); + applySizeOverrideIfNeeded(newParentConfiguration, parentWindowingMode, resolvedConfig); mResolveConfigHint.resetTmpOverrides(); logAppCompatState(); @@ -8708,15 +8706,85 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * TODO: Consider integrate this with computeConfigByResolveHint() */ private void applySizeOverrideIfNeeded(Configuration newParentConfiguration, - Configuration inOutConfig) { - applySizeOverride( - mDisplayContent, - info.applicationInfo, - newParentConfiguration, - inOutConfig, - mOptOutEdgeToEdge, - hasFixedRotationTransform(), - getCompatDisplayInsets() != null); + int parentWindowingMode, Configuration inOutConfig) { + if (mDisplayContent == null) { + return; + } + final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds(); + int rotation = newParentConfiguration.windowConfiguration.getRotation(); + if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) { + rotation = mDisplayContent.getRotation(); + } + if (!mOptOutEdgeToEdge && (!mResolveConfigHint.mUseOverrideInsetsForConfig + || getCompatDisplayInsets() != null + || (isFloating(parentWindowingMode) + // Check the requested windowing mode of activity as well in case it is + // switching between PiP and fullscreen. + && (inOutConfig.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_UNDEFINED + || isFloating(inOutConfig.windowConfiguration.getWindowingMode()))) + || rotation == ROTATION_UNDEFINED)) { + // If the insets configuration decoupled logic is not enabled for the app, or the app + // already has a compat override, or the context doesn't contain enough info to + // calculate the override, skip the override. + return; + } + // Make sure the orientation related fields will be updated by the override insets, because + // fixed rotation has assigned the fields from display's configuration. + if (hasFixedRotationTransform()) { + inOutConfig.windowConfiguration.setAppBounds(null); + inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED; + inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED; + inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; + inOutConfig.orientation = ORIENTATION_UNDEFINED; + } + + // Override starts here. + final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270); + final int dw = rotated ? mDisplayContent.mBaseDisplayHeight + : mDisplayContent.mBaseDisplayWidth; + final int dh = rotated ? mDisplayContent.mBaseDisplayWidth + : mDisplayContent.mBaseDisplayHeight; + final Rect nonDecorInsets = mDisplayContent.getDisplayPolicy() + .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorInsets; + // This should be the only place override the configuration for ActivityRecord. Override + // the value if not calculated yet. + Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); + if (outAppBounds == null || outAppBounds.isEmpty()) { + inOutConfig.windowConfiguration.setAppBounds(parentBounds); + outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); + outAppBounds.inset(nonDecorInsets); + } + float density = inOutConfig.densityDpi; + if (density == Configuration.DENSITY_DPI_UNDEFINED) { + density = newParentConfiguration.densityDpi; + } + density *= DisplayMetrics.DENSITY_DEFAULT_SCALE; + if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) { + inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f); + } + if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) { + inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f); + } + if (inOutConfig.smallestScreenWidthDp + == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED + && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) { + // For the case of PIP transition and multi-window environment, the + // smallestScreenWidthDp is handled already. Override only if the app is in + // fullscreen. + final DisplayInfo info = new DisplayInfo(mDisplayContent.getDisplayInfo()); + mDisplayContent.computeSizeRanges(info, rotated, dw, dh, + mDisplayContent.getDisplayMetrics().density, + inOutConfig, true /* overrideConfig */); + } + + // It's possible that screen size will be considered in different orientation with or + // without considering the system bar insets. Override orientation as well. + if (inOutConfig.orientation == ORIENTATION_UNDEFINED) { + inOutConfig.orientation = + (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp) + ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE; + } } private void computeConfigByResolveHint(@NonNull Configuration resolvedConfig, diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index d65a106a1079..00b6453b2484 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -2737,9 +2737,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { final ActivityOptions activityOptions = options != null ? options.getOptions(this) : null; - boolean moveHomeTaskForward = true; synchronized (mService.mGlobalLock) { final boolean isCallerRecents = mRecentTasks.isCallerRecents(callingUid); + boolean moveHomeTaskForward = isCallerRecents; int activityType = ACTIVITY_TYPE_UNDEFINED; if (activityOptions != null) { activityType = activityOptions.getLaunchActivityType(); diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 14e256f7a815..8421765060ce 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -210,6 +210,9 @@ class BackNavigationController { + "topRunningActivity=%s, callbackInfo=%s, currentFocus=%s", currentTask, currentActivity, callbackInfo, window); + // Clear the pointer down outside focus if any. + mWindowManagerService.clearPointerDownOutsideFocusRunnable(); + // If we don't need to set up the animation, we return early. This is the case when // - We have an application callback. // - We don't have any ActivityRecord or Task to animate. @@ -1361,6 +1364,8 @@ class BackNavigationController { synchronized (openTask.mWmService.mGlobalLock) { if (mRequestedStartingSurfaceId != INVALID_TASK_ID) { mStartingSurface = sc; + } else { + sc.release(); } } } @@ -1599,12 +1604,20 @@ class BackNavigationController { @NonNull ActivityRecord[] visibleOpenActivities) { boolean needsLaunchBehind = true; if (isSupportWindowlessSurface() && mShowWindowlessSurface && !mIsLaunchBehind) { + boolean activitiesAreDrawn = false; + for (int i = visibleOpenActivities.length - 1; i >= 0; --i) { + // If the activity hasn't stopped, it's window should remain drawn. + activitiesAreDrawn |= visibleOpenActivities[i].firstWindowDrawn; + } final WindowContainer mainOpen = openAnimationAdaptor.mAdaptors[0].mTarget; final TaskSnapshot snapshot = getSnapshot(mainOpen, visibleOpenActivities); - openAnimationAdaptor.createStartingSurface(snapshot); - // set LaunchBehind if we are creating splash screen surface. - needsLaunchBehind = snapshot == null - && openAnimationAdaptor.mRequestedStartingSurfaceId != INVALID_TASK_ID; + // Don't create starting surface if previous activities haven't stopped or + // the snapshot does not exist. + if (snapshot != null || !activitiesAreDrawn) { + openAnimationAdaptor.createStartingSurface(snapshot); + } + // Only use LaunchBehind if snapshot does not exist. + needsLaunchBehind = snapshot == null; } if (needsLaunchBehind) { for (int i = visibleOpenActivities.length - 1; i >= 0; --i) { diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java index 70e6d5ddf3da..efd52026cfec 100644 --- a/services/core/java/com/android/server/wm/ConfigurationContainer.java +++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java @@ -22,23 +22,14 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; -import static android.app.WindowConfiguration.isFloating; import static android.app.WindowConfiguration.windowingModeToString; import static android.app.WindowConfigurationProto.WINDOWING_MODE; import static android.content.ConfigurationProto.WINDOW_CONFIGURATION; -import static android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED; -import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION; -import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; -import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.content.res.Configuration.ORIENTATION_UNDEFINED; -import static android.view.Surface.ROTATION_270; -import static android.view.Surface.ROTATION_90; import static com.android.server.wm.ConfigurationContainerProto.FULL_CONFIGURATION; import static com.android.server.wm.ConfigurationContainerProto.MERGED_OVERRIDE_CONFIGURATION; @@ -47,14 +38,11 @@ import static com.android.server.wm.ConfigurationContainerProto.OVERRIDE_CONFIGU import android.annotation.CallSuper; import android.annotation.NonNull; import android.app.WindowConfiguration; -import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.os.LocaleList; -import android.util.DisplayMetrics; import android.util.proto.ProtoOutputStream; -import android.view.DisplayInfo; import com.android.internal.annotations.VisibleForTesting; @@ -536,133 +524,22 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { } /** - * @see ActivityRecord#applySizeOverrideIfNeeded - */ - public static boolean applySizeOverride(DisplayContent displayContent, ApplicationInfo appInfo, - Configuration newParentConfiguration, Configuration inOutConfig, - boolean optOutEdgeToEdge, boolean hasFixedRotationTransform, - boolean hasCompatDisplayInsets) { - if (displayContent == null) { - return false; - } - final boolean useOverrideInsetsForConfig = - displayContent.mWmService.mFlags.mInsetsDecoupledConfiguration - ? !appInfo.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED) - && !appInfo.isChangeEnabled( - OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION) - : appInfo.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION); - if (newParentConfiguration == null) { - newParentConfiguration = displayContent.getConfiguration(); - } - final int parentWindowingMode = - newParentConfiguration.windowConfiguration.getWindowingMode(); - final boolean isFloating = isFloating(parentWindowingMode) - // Check the requested windowing mode of activity as well in case it is - // switching between PiP and fullscreen. - && (inOutConfig.windowConfiguration.getWindowingMode() == WINDOWING_MODE_UNDEFINED - || isFloating(inOutConfig.windowConfiguration.getWindowingMode())); - final Rect parentBounds = newParentConfiguration.windowConfiguration.getBounds(); - int rotation = newParentConfiguration.windowConfiguration.getRotation(); - if (rotation == ROTATION_UNDEFINED && !hasFixedRotationTransform) { - rotation = displayContent.getRotation(); - } - if (!optOutEdgeToEdge && (!useOverrideInsetsForConfig - || hasCompatDisplayInsets - || isFloating - || rotation == ROTATION_UNDEFINED)) { - // If the insets configuration decoupled logic is not enabled for the app, or the app - // already has a compat override, or the context doesn't contain enough info to - // calculate the override, skip the override. - return false; - } - // Make sure the orientation related fields will be updated by the override insets, because - // fixed rotation has assigned the fields from display's configuration. - if (hasFixedRotationTransform) { - inOutConfig.windowConfiguration.setAppBounds(null); - inOutConfig.screenWidthDp = Configuration.SCREEN_WIDTH_DP_UNDEFINED; - inOutConfig.screenHeightDp = Configuration.SCREEN_HEIGHT_DP_UNDEFINED; - inOutConfig.smallestScreenWidthDp = Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; - inOutConfig.orientation = ORIENTATION_UNDEFINED; - } - - // Override starts here. - final boolean rotated = (rotation == ROTATION_90 || rotation == ROTATION_270); - final int dw = rotated - ? displayContent.mBaseDisplayHeight - : displayContent.mBaseDisplayWidth; - final int dh = rotated - ? displayContent.mBaseDisplayWidth - : displayContent.mBaseDisplayHeight; - final Rect nonDecorFrame = displayContent.getDisplayPolicy() - .getDecorInsetsInfo(rotation, dw, dh).mOverrideNonDecorFrame; - // This should be the only place override the configuration for ActivityRecord. Override - // the value if not calculated yet. - Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); - if (outAppBounds == null || outAppBounds.isEmpty()) { - inOutConfig.windowConfiguration.setAppBounds(parentBounds); - outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); - outAppBounds.intersect(nonDecorFrame); - } - float density = inOutConfig.densityDpi; - if (density == Configuration.DENSITY_DPI_UNDEFINED) { - density = newParentConfiguration.densityDpi; - } - density *= DisplayMetrics.DENSITY_DEFAULT_SCALE; - if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) { - inOutConfig.screenWidthDp = (int) (outAppBounds.width() / density + 0.5f); - } - if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) { - inOutConfig.screenHeightDp = (int) (outAppBounds.height() / density + 0.5f); - } - if (inOutConfig.smallestScreenWidthDp == Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED - && parentWindowingMode == WINDOWING_MODE_FULLSCREEN) { - // For the case of PIP transition and multi-window environment, the - // smallestScreenWidthDp is handled already. Override only if the app is in - // fullscreen. - final DisplayInfo info = new DisplayInfo(displayContent.getDisplayInfo()); - displayContent.computeSizeRanges(info, rotated, dw, dh, - displayContent.getDisplayMetrics().density, - inOutConfig, true /* overrideConfig */); - } - - // It's possible that screen size will be considered in different orientation with or - // without considering the system bar insets. Override orientation as well. - if (inOutConfig.orientation == ORIENTATION_UNDEFINED) { - inOutConfig.orientation = - (inOutConfig.screenWidthDp <= inOutConfig.screenHeightDp) - ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE; - } - return true; - } - - /** - * Gives the derived class a chance to apply the app-specific configuration. - * - * @param inOutConfig the configuration as the requested configuration. - * @return true if any of the given configuration has been updated. - */ - public boolean onApplyAppSpecificConfig(Configuration inOutConfig) { - return false; - } - - /** * Applies app-specific nightMode and {@link LocaleList} on requested configuration. * @return true if any of the requested configuration has been updated. */ public boolean applyAppSpecificConfig(Integer nightMode, LocaleList locales, @Configuration.GrammaticalGender Integer gender) { mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration()); - boolean changed = onApplyAppSpecificConfig(mRequestsTmpConfig); boolean newNightModeSet = (nightMode != null) && setOverrideNightMode(mRequestsTmpConfig, nightMode); boolean newLocalesSet = (locales != null) && setOverrideLocales(mRequestsTmpConfig, locales); boolean newGenderSet = setOverrideGender(mRequestsTmpConfig, gender == null ? Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED : gender); - if (changed || newNightModeSet || newLocalesSet || newGenderSet) { + if (newNightModeSet || newLocalesSet || newGenderSet) { onRequestedOverrideConfigurationChanged(mRequestsTmpConfig); } - return changed || newNightModeSet || newLocalesSet || newGenderSet; + return newNightModeSet || newLocalesSet || newGenderSet; } /** diff --git a/services/core/java/com/android/server/wm/UnknownAppVisibilityController.java b/services/core/java/com/android/server/wm/UnknownAppVisibilityController.java index c0713966d8de..3947d02c86c9 100644 --- a/services/core/java/com/android/server/wm/UnknownAppVisibilityController.java +++ b/services/core/java/com/android/server/wm/UnknownAppVisibilityController.java @@ -153,6 +153,10 @@ class UnknownAppVisibilityController { mUnknownApps.put(activity, UNKNOWN_STATE_WAITING_VISIBILITY_UPDATE); mDisplayContent.notifyKeyguardFlagsChanged(); notifyVisibilitiesUpdated(); + } else if (state == UNKNOWN_STATE_WAITING_RESUME + && !activity.isState(ActivityRecord.State.RESUMED)) { + Slog.d(TAG, "UAVC: skip waiting for non-resumed relayouted " + activity); + mUnknownApps.remove(activity); } } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 0c1ec5042c19..9a5f84cf7449 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -732,6 +732,13 @@ public class WindowManagerService extends IWindowManager.Stub final DisplayWindowListenerController mDisplayNotificationController; final TaskSystemBarsListenerController mTaskSystemBarsListenerController; + /** Amount of time (in milliseconds) to delay the pointer down outside focus handling */ + private static final int POINTER_DOWN_OUTSIDE_FOCUS_TIMEOUT_MS = 50; + + /** A runnable to handle pointer down outside focus event. */ + @Nullable + private Runnable mPointerDownOutsideFocusRunnable; + boolean mDisplayFrozen = false; long mDisplayFreezeTime = 0; int mLastDisplayFreezeDuration = 0; @@ -5896,7 +5903,8 @@ public class WindowManagerService extends IWindowManager.Stub case ON_POINTER_DOWN_OUTSIDE_FOCUS: { synchronized (mGlobalLock) { final IBinder touchedToken = (IBinder) msg.obj; - onPointerDownOutsideFocusLocked(getInputTargetFromToken(touchedToken)); + onPointerDownOutsideFocusLocked(getInputTargetFromToken(touchedToken), + true /* fromHandler */); } break; } @@ -7922,7 +7930,8 @@ public class WindowManagerService extends IWindowManager.Stub synchronized (mGlobalLock) { final InputTarget inputTarget = WindowManagerService.this.getInputTargetFromWindowTokenLocked(windowToken); - WindowManagerService.this.onPointerDownOutsideFocusLocked(inputTarget); + WindowManagerService.this.onPointerDownOutsideFocusLocked(inputTarget, + false /* fromHandler */); } } @@ -8997,39 +9006,78 @@ public class WindowManagerService extends IWindowManager.Stub } } - private void onPointerDownOutsideFocusLocked(InputTarget t) { + void clearPointerDownOutsideFocusRunnable() { + if (mPointerDownOutsideFocusRunnable == null) return; + + mH.removeCallbacks(mPointerDownOutsideFocusRunnable); + mPointerDownOutsideFocusRunnable = null; + } + + private void onPointerDownOutsideFocusLocked(InputTarget t, boolean fromHandler) { if (t == null || !t.receiveFocusFromTapOutside()) { // If the window that received the input event cannot receive keys, don't move the // display it's on to the top since that window won't be able to get focus anyway. return; } - if (mRecentsAnimationController != null - && mRecentsAnimationController.getTargetAppMainWindow() == t) { - // If there is an active recents animation and touched window is the target, then ignore - // the touch. The target already handles touches using its own input monitor and we - // don't want to trigger any lifecycle changes from focusing another window. - // TODO(b/186770026): We should remove this once we support multiple resumed activities - // while in overview - return; - } + clearPointerDownOutsideFocusRunnable(); + + // For embedded activity that is showing side-by-side with another activity, delay + // handling the touch-outside event to prevent focus rapid changes back-n-forth. + // Otherwise, handle the touch-outside event directly. final WindowState w = t.getWindowState(); - if (w != null) { - final Task task = w.getTask(); - if (task != null && w.mTransitionController.isTransientHide(task)) { - // Don't disturb transient animation by accident touch. + final ActivityRecord activity = w != null ? w.getActivityRecord() : null; + if (activity != null && activity.isEmbedded() + && activity.getTaskFragment().getAdjacentTaskFragment() != null) { + mPointerDownOutsideFocusRunnable = () -> handlePointerDownOutsideFocus(t); + mH.postDelayed(mPointerDownOutsideFocusRunnable, POINTER_DOWN_OUTSIDE_FOCUS_TIMEOUT_MS); + } else if (!fromHandler) { + // Still post the runnable to handler thread in case there is already a runnable + // in execution, but still waiting to hold the wm lock. + mPointerDownOutsideFocusRunnable = () -> handlePointerDownOutsideFocus(t); + mH.post(mPointerDownOutsideFocusRunnable); + } else { + handlePointerDownOutsideFocus(t); + } + } + + private void handlePointerDownOutsideFocus(InputTarget t) { + synchronized (mGlobalLock) { + if (mPointerDownOutsideFocusRunnable != null + && mH.hasCallbacks(mPointerDownOutsideFocusRunnable)) { + // Skip if there's another pending pointer-down-outside-focus event. + return; + } + clearPointerDownOutsideFocusRunnable(); + + if (mRecentsAnimationController != null + && mRecentsAnimationController.getTargetAppMainWindow() == t) { + // If there is an active recents animation and touched window is the target, + // then ignore the touch. The target already handles touches using its own + // input monitor and we don't want to trigger any lifecycle changes from + // focusing another window. + // TODO(b/186770026): We should remove this once we support multiple resumed + // activities while in overview return; } - } - ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "onPointerDownOutsideFocusLocked called on %s", - t); - if (mFocusedInputTarget != t && mFocusedInputTarget != null) { - mFocusedInputTarget.handleTapOutsideFocusOutsideSelf(); + final WindowState w = t.getWindowState(); + if (w != null) { + final Task task = w.getTask(); + if (task != null && w.mTransitionController.isTransientHide(task)) { + // Don't disturb transient animation by accident touch. + return; + } + } + + ProtoLog.i(WM_DEBUG_FOCUS_LIGHT, "onPointerDownOutsideFocusLocked called on %s", t); + if (mFocusedInputTarget != t && mFocusedInputTarget != null) { + mFocusedInputTarget.handleTapOutsideFocusOutsideSelf(); + } + // Trigger Activity#onUserLeaveHint() if the order change of task pauses any activities. + mAtmService.mTaskSupervisor.mUserLeaving = true; + t.handleTapOutsideFocusInsideSelf(); + mAtmService.mTaskSupervisor.mUserLeaving = false; } - // Trigger Activity#onUserLeaveHint() if the order change of task pauses any activities. - mAtmService.mTaskSupervisor.mUserLeaving = true; - t.handleTapOutsideFocusInsideSelf(); - mAtmService.mTaskSupervisor.mUserLeaving = false; } @VisibleForTesting diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index f6942ee42773..60d3e787cac4 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -1581,25 +1581,6 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } @Override - public boolean onApplyAppSpecificConfig(Configuration inOutConfig) { - if (mConfigActivityRecord != null) { - // Let the activity decide whether to apply the size override. - return false; - } - final DisplayContent displayContent = mAtm.mWindowManager != null - ? mAtm.mWindowManager.getDefaultDisplayContentLocked() - : null; - return applySizeOverride( - displayContent, - mInfo, - null /* newParentConfiguration */, - inOutConfig, - false /* optOutEdgeToEdge */, - false /* hasFixedRotationTransform */, - false /* hasCompatDisplayInsets */); - } - - @Override public void onConfigurationChanged(Configuration newGlobalConfig) { super.onConfigurationChanged(newGlobalConfig); diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java index 64dbc50b8dd2..c76bcf804ee4 100644 --- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java +++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java @@ -287,7 +287,7 @@ public final class ProfcollectForwardingService extends SystemService { if (randomNum < traceFrequency) { BackgroundThread.get().getThreadHandler().post(() -> { try { - mIProfcollect.trace_once("applaunch"); + mIProfcollect.trace_system("applaunch"); } catch (RemoteException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } @@ -327,7 +327,7 @@ public final class ProfcollectForwardingService extends SystemService { // Dex2oat could take a while before it starts. Add a short delay before start tracing. BackgroundThread.get().getThreadHandler().postDelayed(() -> { try { - mIProfcollect.trace_once("dex2oat"); + mIProfcollect.trace_system("dex2oat"); } catch (RemoteException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } @@ -404,7 +404,7 @@ public final class ProfcollectForwardingService extends SystemService { String traceTag = traceInitialization ? "camera_init" : "camera"; BackgroundThread.get().getThreadHandler().postDelayed(() -> { try { - mIProfcollect.trace_once(traceTag); + mIProfcollect.trace_system(traceTag); } catch (RemoteException e) { Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java index 35cff7a7a324..7efbc88833cf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ProtoLogIntegrationTest.java @@ -25,11 +25,11 @@ import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; -import com.android.internal.protolog.ProtoLog; import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.ProtoLogImpl; import com.android.internal.protolog.common.IProtoLog; import com.android.internal.protolog.common.LogLevel; +import com.android.internal.protolog.ProtoLog; import org.junit.After; import org.junit.Ignore; @@ -53,6 +53,9 @@ public class ProtoLogIntegrationTest { runWith(mockedProtoLog, this::testProtoLog); verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq(ProtoLogGroup.TEST_GROUP), anyInt(), eq(0b0010010111), + eq(com.android.internal.protolog.ProtoLogGroup.TEST_GROUP.isLogToLogcat() + ? "Test completed successfully: %b %d %x %f %% %s" + : null), eq(new Object[]{true, 1L, 2L, 0.3, "ok"})); } diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java index 5a48327e7576..5a27593c7a36 100644 --- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java @@ -59,6 +59,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.LinkedList; +import java.util.TreeMap; /** * Test class for {@link ProtoLogImpl}. @@ -89,7 +90,7 @@ public class LegacyProtoLogImplTest { //noinspection ResultOfMethodCallIgnored mFile.delete(); mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename, - 1024 * 1024, mReader, 1024, () -> {}); + 1024 * 1024, mReader, 1024, new TreeMap<>(), () -> {}); } @After @@ -141,7 +142,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, new Object[]{true, 10000, 30000, "test", 0.000003}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -158,7 +159,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, new Object[]{true, 10000, 0.0001, 0.00002, "test"}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -175,7 +176,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", new Object[]{5}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -191,7 +192,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, new Object[]{5}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -207,7 +208,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", new Object[]{5}); verify(implSpy, never()).passToLogcat(any(), any(), any()); @@ -276,7 +277,7 @@ public class LegacyProtoLogImplTest { long before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, - 0b1110101001010100, + 0b1110101001010100, null, new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true}); long after = SystemClock.elapsedRealtimeNanos(); mProtoLog.stopProtoLog(mock(PrintWriter.class), true); @@ -301,7 +302,7 @@ public class LegacyProtoLogImplTest { long before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, - 0b01100100, + 0b01100100, null, new Object[]{"test", 1, 0.1, true}); long after = SystemClock.elapsedRealtimeNanos(); mProtoLog.stopProtoLog(mock(PrintWriter.class), true); @@ -325,7 +326,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); mProtoLog.startProtoLog(mock(PrintWriter.class)); mProtoLog.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, - 0b11, new Object[]{true}); + 0b11, null, new Object[]{true}); mProtoLog.stopProtoLog(mock(PrintWriter.class), true); try (InputStream is = new FileInputStream(mFile)) { ProtoInputStream ip = new ProtoInputStream(is); diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java index b6672a0e2f4b..1d7b6b348e10 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java @@ -60,9 +60,6 @@ import org.junit.runners.JUnit4; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import perfetto.protos.Protolog; -import perfetto.protos.ProtologCommon; - import java.io.File; import java.io.IOException; import java.util.List; @@ -70,6 +67,9 @@ import java.util.Random; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; +import perfetto.protos.Protolog; +import perfetto.protos.ProtologCommon; + /** * Test class for {@link ProtoLogImpl}. */ @@ -111,9 +111,6 @@ public class PerfettoProtoLogImplTest { //noinspection ResultOfMethodCallIgnored mFile.delete(); - TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false); - TestProtoLogGroup.TEST_GROUP.setLogToProto(false); - mViewerConfigBuilder = Protolog.ProtoLogViewerConfig.newBuilder() .addGroups( Protolog.ProtoLogViewerConfig.Group.newBuilder() @@ -160,9 +157,8 @@ public class PerfettoProtoLogImplTest { mCacheUpdater = () -> {}; mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider)); mProtoLog = new PerfettoProtoLogImpl( - viewerConfigInputStreamProvider, mReader, + viewerConfigInputStreamProvider, mReader, new TreeMap<>(), () -> mCacheUpdater.run()); - mProtoLog.registerGroups(TestProtoLogGroup.values()); } @After @@ -214,15 +210,15 @@ public class PerfettoProtoLogImplTest { // Shouldn't be logging anything except WTF unless explicitly requested in the group // override. mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -244,15 +240,15 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -278,15 +274,15 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -308,15 +304,15 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, new Object[]{true}); + LogDataType.BOOLEAN, null, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -333,14 +329,14 @@ public class PerfettoProtoLogImplTest { } @Test - public void log_logcatEnabled() { + public void log_logcatEnabledExternalMessage() { when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f"); PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, new Object[]{true, 10000, 30000, "test", 0.000003}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -357,17 +353,32 @@ public class PerfettoProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, new Object[]{true, 10000, 0.0001, 0.00002, "test"}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( LogLevel.INFO), - eq("FORMAT_ERROR \"test %b %d %% %x %s %f\", " - + "args=(true, 10000, 1.0E-4, 2.0E-5, test)")); + eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test")); verify(mReader).getViewerString(eq(1234L)); } @Test + public void log_logcatEnabledInlineMessage() { + when(mReader.getViewerString(anyLong())).thenReturn("test %d"); + PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); + TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); + TestProtoLogGroup.TEST_GROUP.setLogToProto(false); + + implSpy.log( + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", + new Object[]{5}); + + verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( + LogLevel.INFO), eq("test 5")); + verify(mReader, never()).getViewerString(anyLong()); + } + + @Test public void log_logcatEnabledNoMessage() { when(mReader.getViewerString(anyLong())).thenReturn(null); PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); @@ -375,11 +386,11 @@ public class PerfettoProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, new Object[]{5}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( - LogLevel.INFO), eq("UNKNOWN MESSAGE#1234 (5)")); + LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5")); verify(mReader).getViewerString(eq(1234L)); } @@ -390,7 +401,7 @@ public class PerfettoProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", new Object[]{5}); verify(implSpy, never()).passToLogcat(any(), any(), any()); @@ -414,7 +425,7 @@ public class PerfettoProtoLogImplTest { before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, - 0b1110101001010100, + 0b1110101001010100, null, new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true}); after = SystemClock.elapsedRealtimeNanos(); } finally { @@ -433,38 +444,6 @@ public class PerfettoProtoLogImplTest { .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, 5.000000e-01, 0.6, true"); } - @Test - public void log_noProcessing() throws IOException { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog().build(); - long before; - long after; - try { - traceMonitor.start(); - assertTrue(mProtoLog.isProtoEnabled()); - - before = SystemClock.elapsedRealtimeNanos(); - mProtoLog.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, - "My test message :: %s, %d, %o, %x, %f, %b", - "test", 1, 2, 3, 0.4, true); - after = SystemClock.elapsedRealtimeNanos(); - } finally { - traceMonitor.stop(mWriter); - } - - final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); - final ProtoLogTrace protolog = reader.readProtoLogTrace(); - - Truth.assertThat(protolog.messages).hasSize(1); - Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos()) - .isAtLeast(before); - Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos()) - .isAtMost(after); - Truth.assertThat(protolog.messages.getFirst().getMessage()) - .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, true"); - } - private long addMessageToConfig(ProtologCommon.ProtoLogLevel logLevel, String message) { final long messageId = new Random().nextLong(); mViewerConfigBuilder.addMessages(Protolog.ProtoLogViewerConfig.MessageData.newBuilder() @@ -491,7 +470,7 @@ public class PerfettoProtoLogImplTest { before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, - 0b01100100, + 0b01100100, null, new Object[]{"test", 1, 0.1, true}); after = SystemClock.elapsedRealtimeNanos(); } finally { @@ -509,7 +488,7 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - 0b11, new Object[]{true}); + 0b11, null, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -533,7 +512,7 @@ public class PerfettoProtoLogImplTest { ProtoLogImpl.setSingleInstance(mProtoLog); ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1, - 0b11, true); + 0b11, null, true); } finally { traceMonitor.stop(mWriter); } @@ -607,7 +586,7 @@ public class PerfettoProtoLogImplTest { .isFalse(); Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isFalse(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isTrue(); PerfettoTraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder().enableProtoLog(true, @@ -685,53 +664,7 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isFalse(); Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) - .isFalse(); - } - - @Test - public void supportsNullString() throws IOException { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog(true) - .build(); - - try { - traceMonitor.start(); - - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, - "My test null string: %s", null); - } finally { - traceMonitor.stop(mWriter); - } - - final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); - final ProtoLogTrace protolog = reader.readProtoLogTrace(); - - Truth.assertThat(protolog.messages).hasSize(1); - Truth.assertThat(protolog.messages.get(0).getMessage()) - .isEqualTo("My test null string: null"); - } - - @Test - public void supportNullParams() throws IOException { - PerfettoTraceMonitor traceMonitor = - PerfettoTraceMonitor.newBuilder().enableProtoLog(true) - .build(); - - try { - traceMonitor.start(); - - mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, - "My null args: %d, %f, %b", null, null, null); - } finally { - traceMonitor.stop(mWriter); - } - - final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); - final ProtoLogTrace protolog = reader.readProtoLogTrace(); - - Truth.assertThat(protolog.messages).hasSize(1); - Truth.assertThat(protolog.messages.get(0).getMessage()) - .isEqualTo("My null args: 0, 0, false"); + .isTrue(); } private enum TestProtoLogGroup implements IProtoLogGroup { diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java index 0496240f01e4..60456f9ea10f 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java @@ -58,50 +58,51 @@ public class ProtoLogImplTest { public void d_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321); + ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); verify(mockedProtoLog).log(eq(LogLevel.DEBUG), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq(new Object[]{})); + eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); } @Test public void v_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321); + ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); verify(mockedProtoLog).log(eq(LogLevel.VERBOSE), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq(new Object[]{})); + eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); } @Test public void i_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321); + ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); verify(mockedProtoLog).log(eq(LogLevel.INFO), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq(new Object[]{})); + eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); } @Test public void w_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234, 4321); + ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234, + 4321, "test %d"); verify(mockedProtoLog).log(eq(LogLevel.WARN), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq(new Object[]{})); + eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); } @Test public void e_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321); + ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq(new Object[]{})); + eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); } @Test @@ -109,10 +110,10 @@ public class ProtoLogImplTest { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); ProtoLogImpl.wtf(TestProtoLogGroup.TEST_GROUP, - 1234, 4321); + 1234, 4321, "test %d"); verify(mockedProtoLog).log(eq(LogLevel.WTF), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq(new Object[]{})); + eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); } private enum TestProtoLogGroup implements IProtoLogGroup { diff --git a/tools/aapt2/xml/XmlPullParser.cpp b/tools/aapt2/xml/XmlPullParser.cpp index 203832d2dbe8..8abc26d67c1f 100644 --- a/tools/aapt2/xml/XmlPullParser.cpp +++ b/tools/aapt2/xml/XmlPullParser.cpp @@ -14,11 +14,13 @@ * limitations under the License. */ -#include <iostream> +#include "xml/XmlPullParser.h" + +#include <algorithm> #include <string> +#include <tuple> #include "util/Util.h" -#include "xml/XmlPullParser.h" #include "xml/XmlUtil.h" using ::android::InputStream; @@ -325,5 +327,18 @@ std::optional<StringPiece> FindNonEmptyAttribute(const XmlPullParser* parser, St return {}; } +XmlPullParser::const_iterator XmlPullParser::FindAttribute(android::StringPiece namespace_uri, + android::StringPiece name) const { + const auto end_iter = end_attributes(); + const auto iter = std::lower_bound(begin_attributes(), end_iter, std::tuple(namespace_uri, name), + [](const Attribute& attr, const auto& rhs) { + return std::tie(attr.namespace_uri, attr.name) < rhs; + }); + if (iter != end_iter && namespace_uri == iter->namespace_uri && name == iter->name) { + return iter; + } + return end_iter; +} + } // namespace xml } // namespace aapt diff --git a/tools/aapt2/xml/XmlPullParser.h b/tools/aapt2/xml/XmlPullParser.h index 655e6dc93e75..64274d032c61 100644 --- a/tools/aapt2/xml/XmlPullParser.h +++ b/tools/aapt2/xml/XmlPullParser.h @@ -19,8 +19,7 @@ #include <expat.h> -#include <algorithm> -#include <istream> +#include <optional> #include <ostream> #include <queue> #include <stack> @@ -302,31 +301,6 @@ inline bool XmlPullParser::Attribute::operator!=(const Attribute& rhs) const { return compare(rhs) != 0; } -inline XmlPullParser::const_iterator XmlPullParser::FindAttribute( - android::StringPiece namespace_uri, android::StringPiece name) const { - const auto end_iter = end_attributes(); - const auto iter = std::lower_bound( - begin_attributes(), end_iter, - std::pair<android::StringPiece, android::StringPiece>(namespace_uri, name), - [](const Attribute& attr, - const std::pair<android::StringPiece, android::StringPiece>& rhs) -> bool { - int cmp = attr.namespace_uri.compare( - 0, attr.namespace_uri.size(), rhs.first.data(), rhs.first.size()); - if (cmp < 0) return true; - if (cmp > 0) return false; - cmp = attr.name.compare(0, attr.name.size(), rhs.second.data(), - rhs.second.size()); - if (cmp < 0) return true; - return false; - }); - - if (iter != end_iter && namespace_uri == iter->namespace_uri && - name == iter->name) { - return iter; - } - return end_iter; -} - } // namespace xml } // namespace aapt diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt index 3c99e68cd6a0..1087ae6ee41d 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogCallProcessorImpl.kt @@ -120,8 +120,6 @@ class ProtoLogCallProcessorImpl( logCallVisitor?.processCall(call, messageString, getLevelForMethodName( call.name.toString(), call, context), groupMap.getValue(groupName)) - } else if (call.name.id == "initialize") { - // No processing } else { // Process non-log message calls otherCallVisitor?.processCall(call) diff --git a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt index c478f5844de6..6a8a0717b2f1 100644 --- a/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt +++ b/tools/protologtool/src/com/android/protolog/tool/SourceTransformer.kt @@ -130,27 +130,28 @@ class SourceTransformer( val hash = CodeUtils.hash(packagePath, messageString, level, group) val newCall = call.clone() - // Remove message string. - // Out: ProtoLog.e(GROUP, args) - newCall.arguments.removeAt(1) + if (!group.textEnabled) { + // Remove message string if text logging is not enabled by default. + // Out: ProtoLog.e(GROUP, null, arg) + newCall.arguments[1].replace(NameExpr("null")) + } // Insert message string hash as a second argument. - // Out: ProtoLog.e(GROUP, 1234, args) + // Out: ProtoLog.e(GROUP, 1234, null, arg) newCall.arguments.add(1, LongLiteralExpr("" + hash + "L")) val argTypes = LogDataType.parseFormatString(messageString) val typeMask = LogDataType.logDataTypesToBitMask(argTypes) // Insert bitmap representing which Number parameters are to be considered as // floating point numbers. - // Out: ProtoLog.e(GROUP, 1234, 0, args) + // Out: ProtoLog.e(GROUP, 1234, 0, null, arg) newCall.arguments.add(2, IntegerLiteralExpr(typeMask)) // Replace call to a stub method with an actual implementation. - // Out: ProtoLogImpl.e(GROUP, 1234, 0, args) + // Out: ProtoLogImpl.e(GROUP, 1234, null, arg) newCall.setScope(protoLogImplClassNode) if (argTypes.size != call.arguments.size - 2) { throw InvalidProtoLogCallException( "Number of arguments (${argTypes.size} does not match format" + " string in: $call", ParsingContext(path, call)) } - val argsOffset = 3 val blockStmt = BlockStmt() if (argTypes.isNotEmpty()) { // Assign every argument to a variable to check its type in compile time @@ -159,9 +160,9 @@ class SourceTransformer( argTypes.forEachIndexed { idx, type -> val varName = "protoLogParam$idx" val declaration = VariableDeclarator(getASTTypeForDataType(type), varName, - getConversionForType(type)(newCall.arguments[idx + argsOffset].clone())) + getConversionForType(type)(newCall.arguments[idx + 4].clone())) blockStmt.addStatement(ExpressionStmt(VariableDeclarationExpr(declaration))) - newCall.setArgument(idx + argsOffset, NameExpr(SimpleName(varName))) + newCall.setArgument(idx + 4, NameExpr(SimpleName(varName))) } } else { // Assign (Object[])null as the vararg parameter to prevent allocating an empty diff --git a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt index 0cbbd483fe59..2a8367787ba1 100644 --- a/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt +++ b/tools/protologtool/tests/com/android/protolog/tool/EndToEndTest.kt @@ -60,7 +60,7 @@ class EndToEndTest { .containsMatch(Pattern.compile("\\{ String protoLogParam0 = " + "String\\.valueOf\\(argString\\); long protoLogParam1 = argInt; " + "com\\.android\\.internal\\.protolog.ProtoLogImpl_.*\\.d\\(" + - "GROUP, -6872339441335321086L, 4, protoLogParam0, protoLogParam1" + + "GROUP, -6872339441335321086L, 4, null, protoLogParam0, protoLogParam1" + "\\); \\}")) } diff --git a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt index 6cde7a72db53..82aa93da613b 100644 --- a/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt +++ b/tools/protologtool/tests/com/android/protolog/tool/SourceTransformerTest.kt @@ -76,7 +76,7 @@ class SourceTransformerTest { class Test { void test() { - if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } } } """.trimIndent() @@ -86,7 +86,7 @@ class SourceTransformerTest { class Test { void test() { - if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, protoLogParam0, protoLogParam1, protoLogParam2); + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, "test %d %f " + "abc %s\n test", protoLogParam0, protoLogParam1, protoLogParam2); } } @@ -98,8 +98,8 @@ class SourceTransformerTest { class Test { void test() { - if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); } - if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } /* ProtoLog.w(TEST_GROUP, "test %d %f", 100, 0.1); */ if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, "test %d %f", protoLogParam0, protoLogParam1); } } } """.trimIndent() @@ -109,7 +109,7 @@ class SourceTransformerTest { class Test { void test() { - if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { org.example.ProtoLogImpl.w(TEST_GROUP, 3218600869538902408L, 0, (Object[]) null); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { org.example.ProtoLogImpl.w(TEST_GROUP, 3218600869538902408L, 0, "test", (Object[]) null); } } } """.trimIndent() @@ -119,7 +119,7 @@ class SourceTransformerTest { class Test { void test() { - if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, protoLogParam0, protoLogParam1); } + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; org.example.ProtoLogImpl.w(TEST_GROUP, -1473209266730422156L, 9, null, protoLogParam0, protoLogParam1); } } } """.trimIndent() @@ -129,7 +129,7 @@ class SourceTransformerTest { class Test { void test() { - if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, protoLogParam0, protoLogParam1, protoLogParam2); + if (org.example.ProtoLogImpl.Cache.TEST_GROUP_enabled[3]) { long protoLogParam0 = 100; double protoLogParam1 = 0.1; String protoLogParam2 = String.valueOf("test"); org.example.ProtoLogImpl.w(TEST_GROUP, -4447034859795564700L, 9, null, protoLogParam0, protoLogParam1, protoLogParam2); } } @@ -172,12 +172,13 @@ class SourceTransformerTest { Truth.assertThat(protoLogCalls).hasSize(1) val methodCall = protoLogCalls[0] as MethodCallExpr assertEquals("w", methodCall.name.asString()) - assertEquals(5, methodCall.arguments.size) + assertEquals(6, methodCall.arguments.size) assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) assertEquals("-1473209266730422156L", methodCall.arguments[1].toString()) assertEquals(0b1001.toString(), methodCall.arguments[2].toString()) - assertEquals("protoLogParam0", methodCall.arguments[3].toString()) - assertEquals("protoLogParam1", methodCall.arguments[4].toString()) + assertEquals("\"test %d %f\"", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) assertEquals(TRANSFORMED_CODE_TEXT_ENABLED, out) } @@ -213,12 +214,13 @@ class SourceTransformerTest { Truth.assertThat(protoLogCalls).hasSize(3) val methodCall = protoLogCalls[0] as MethodCallExpr assertEquals("w", methodCall.name.asString()) - assertEquals(5, methodCall.arguments.size) + assertEquals(6, methodCall.arguments.size) assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) assertEquals("-1473209266730422156L", methodCall.arguments[1].toString()) assertEquals(0b1001.toString(), methodCall.arguments[2].toString()) - assertEquals("protoLogParam0", methodCall.arguments[3].toString()) - assertEquals("protoLogParam1", methodCall.arguments[4].toString()) + assertEquals("\"test %d %f\"", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) assertEquals(TRANSFORMED_CODE_MULTICALL_TEXT, out) } @@ -250,13 +252,13 @@ class SourceTransformerTest { Truth.assertThat(protoLogCalls).hasSize(1) val methodCall = protoLogCalls[0] as MethodCallExpr assertEquals("w", methodCall.name.asString()) - assertEquals(6, methodCall.arguments.size) + assertEquals(7, methodCall.arguments.size) assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) assertEquals("-4447034859795564700L", methodCall.arguments[1].toString()) assertEquals(0b001001.toString(), methodCall.arguments[2].toString()) - assertEquals("protoLogParam0", methodCall.arguments[3].toString()) - assertEquals("protoLogParam1", methodCall.arguments[4].toString()) - assertEquals("protoLogParam2", methodCall.arguments[5].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals("protoLogParam2", methodCall.arguments[6].toString()) assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_ENABLED, out) } @@ -287,7 +289,7 @@ class SourceTransformerTest { Truth.assertThat(protoLogCalls).hasSize(1) val methodCall = protoLogCalls[0] as MethodCallExpr assertEquals("w", methodCall.name.asString()) - assertEquals(4, methodCall.arguments.size) + assertEquals(5, methodCall.arguments.size) assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) assertEquals("3218600869538902408L", methodCall.arguments[1].toString()) assertEquals(0.toString(), methodCall.arguments[2].toString()) @@ -321,12 +323,13 @@ class SourceTransformerTest { Truth.assertThat(protoLogCalls).hasSize(1) val methodCall = protoLogCalls[0] as MethodCallExpr assertEquals("w", methodCall.name.asString()) - assertEquals(5, methodCall.arguments.size) + assertEquals(6, methodCall.arguments.size) assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) assertEquals("-1473209266730422156L", methodCall.arguments[1].toString()) assertEquals(0b1001.toString(), methodCall.arguments[2].toString()) - assertEquals("protoLogParam0", methodCall.arguments[3].toString()) - assertEquals("protoLogParam1", methodCall.arguments[4].toString()) + assertEquals("null", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) assertEquals(TRANSFORMED_CODE_TEXT_DISABLED, out) } @@ -358,13 +361,14 @@ class SourceTransformerTest { Truth.assertThat(protoLogCalls).hasSize(1) val methodCall = protoLogCalls[0] as MethodCallExpr assertEquals("w", methodCall.name.asString()) - assertEquals(6, methodCall.arguments.size) + assertEquals(7, methodCall.arguments.size) assertEquals("TEST_GROUP", methodCall.arguments[0].toString()) assertEquals("-4447034859795564700L", methodCall.arguments[1].toString()) assertEquals(0b001001.toString(), methodCall.arguments[2].toString()) - assertEquals("protoLogParam0", methodCall.arguments[3].toString()) - assertEquals("protoLogParam1", methodCall.arguments[4].toString()) - assertEquals("protoLogParam2", methodCall.arguments[5].toString()) + assertEquals("null", methodCall.arguments[3].toString()) + assertEquals("protoLogParam0", methodCall.arguments[4].toString()) + assertEquals("protoLogParam1", methodCall.arguments[5].toString()) + assertEquals("protoLogParam2", methodCall.arguments[6].toString()) assertEquals(TRANSFORMED_CODE_MULTILINE_TEXT_DISABLED, out) } } |