diff options
381 files changed, 13321 insertions, 3684 deletions
diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java index d1e9e4e4289b..50f4ddd5e2e3 100644 --- a/apex/media/framework/java/android/media/MediaParser.java +++ b/apex/media/framework/java/android/media/MediaParser.java @@ -1412,14 +1412,12 @@ public final class MediaParser { setOptionalMediaFormatInt(result, MediaFormat.KEY_HEIGHT, format.height); List<byte[]> initData = format.initializationData; - if (initData != null) { - for (int i = 0; i < initData.size(); i++) { - result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i))); - } + for (int i = 0; i < initData.size(); i++) { + result.setByteBuffer("csd-" + i, ByteBuffer.wrap(initData.get(i))); } + setPcmEncoding(format, result); setOptionalMediaFormatString(result, MediaFormat.KEY_LANGUAGE, format.language); setOptionalMediaFormatInt(result, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); - setOptionalMediaFormatInt(result, MediaFormat.KEY_PCM_ENCODING, format.pcmEncoding); setOptionalMediaFormatInt(result, MediaFormat.KEY_ROTATION, format.rotationDegrees); setOptionalMediaFormatInt(result, MediaFormat.KEY_SAMPLE_RATE, format.sampleRate); setOptionalMediaFormatInt( @@ -1462,6 +1460,27 @@ public final class MediaParser { return result; } + private static void setPcmEncoding(Format format, MediaFormat result) { + int exoPcmEncoding = format.pcmEncoding; + setOptionalMediaFormatInt(result, "exo-pcm-encoding", format.pcmEncoding); + int mediaFormatPcmEncoding; + switch (exoPcmEncoding) { + case C.ENCODING_PCM_8BIT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_8BIT; + break; + case C.ENCODING_PCM_16BIT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_16BIT; + break; + case C.ENCODING_PCM_FLOAT: + mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_FLOAT; + break; + default: + // No matching value. Do nothing. + return; + } + result.setInteger(MediaFormat.KEY_PCM_ENCODING, mediaFormatPcmEncoding); + } + private static void setOptionalMediaFormatInt(MediaFormat mediaFormat, String key, int value) { if (value != Format.NO_VALUE) { mediaFormat.setInteger(key, value); diff --git a/apex/statsd/aidl/android/os/IStatsd.aidl b/apex/statsd/aidl/android/os/IStatsd.aidl index 80308d26a430..0d3f4208a2ab 100644 --- a/apex/statsd/aidl/android/os/IStatsd.aidl +++ b/apex/statsd/aidl/android/os/IStatsd.aidl @@ -182,12 +182,6 @@ interface IStatsd { void unsetBroadcastSubscriber(long configId, long subscriberId, int callingUid); /** - * Apps can send an atom via this application breadcrumb with the specified label and state for - * this label. This allows building custom metrics and predicates. - */ - void sendAppBreadcrumbAtom(int label, int state); - - /** * Tell the stats daemon that all the pullers registered during boot have been sent. */ oneway void allPullersFromBootRegistered(); diff --git a/apex/statsd/framework/java/android/util/StatsLog.java b/apex/statsd/framework/java/android/util/StatsLog.java index 536b71a0e625..4eeae57fe195 100644 --- a/apex/statsd/framework/java/android/util/StatsLog.java +++ b/apex/statsd/framework/java/android/util/StatsLog.java @@ -25,8 +25,7 @@ import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.Context; import android.os.IStatsd; -import android.os.RemoteException; -import android.os.StatsFrameworkInitializer; +import android.os.Process; import android.util.proto.ProtoOutputStream; import com.android.internal.util.StatsdStatsLog; @@ -45,10 +44,6 @@ public final class StatsLog { private static final boolean DEBUG = false; private static final int EXPERIMENT_IDS_FIELD_ID = 1; - private static IStatsd sService; - - private static Object sLogLock = new Object(); - private StatsLog() { } @@ -59,26 +54,13 @@ public final class StatsLog { * @return True if the log request was sent to statsd. */ public static boolean logStart(int label) { - synchronized (sLogLock) { - try { - IStatsd service = getIStatsdLocked(); - if (service == null) { - if (DEBUG) { - Log.d(TAG, "Failed to find statsd when logging start"); - } - return false; - } - service.sendAppBreadcrumbAtom(label, - StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__START); - return true; - } catch (RemoteException e) { - sService = null; - if (DEBUG) { - Log.d(TAG, "Failed to connect to statsd when logging start"); - } - return false; - } - } + int callingUid = Process.myUid(); + StatsdStatsLog.write( + StatsdStatsLog.APP_BREADCRUMB_REPORTED, + callingUid, + label, + StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__START); + return true; } /** @@ -88,26 +70,13 @@ public final class StatsLog { * @return True if the log request was sent to statsd. */ public static boolean logStop(int label) { - synchronized (sLogLock) { - try { - IStatsd service = getIStatsdLocked(); - if (service == null) { - if (DEBUG) { - Log.d(TAG, "Failed to find statsd when logging stop"); - } - return false; - } - service.sendAppBreadcrumbAtom( - label, StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__STOP); - return true; - } catch (RemoteException e) { - sService = null; - if (DEBUG) { - Log.d(TAG, "Failed to connect to statsd when logging stop"); - } - return false; - } - } + int callingUid = Process.myUid(); + StatsdStatsLog.write( + StatsdStatsLog.APP_BREADCRUMB_REPORTED, + callingUid, + label, + StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__STOP); + return true; } /** @@ -117,26 +86,13 @@ public final class StatsLog { * @return True if the log request was sent to statsd. */ public static boolean logEvent(int label) { - synchronized (sLogLock) { - try { - IStatsd service = getIStatsdLocked(); - if (service == null) { - if (DEBUG) { - Log.d(TAG, "Failed to find statsd when logging event"); - } - return false; - } - service.sendAppBreadcrumbAtom( - label, StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__UNSPECIFIED); - return true; - } catch (RemoteException e) { - sService = null; - if (DEBUG) { - Log.d(TAG, "Failed to connect to statsd when logging event"); - } - return false; - } - } + int callingUid = Process.myUid(); + StatsdStatsLog.write( + StatsdStatsLog.APP_BREADCRUMB_REPORTED, + callingUid, + label, + StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__UNSPECIFIED); + return true; } /** @@ -181,17 +137,6 @@ public final class StatsLog { return true; } - private static IStatsd getIStatsdLocked() throws RemoteException { - if (sService != null) { - return sService; - } - sService = IStatsd.Stub.asInterface(StatsFrameworkInitializer - .getStatsServiceManager() - .getStatsdServiceRegisterer() - .get()); - return sService; - } - /** * Write an event to stats log using the raw format. * diff --git a/api/current.txt b/api/current.txt index 80e2d00b3e48..3f8bee9230a6 100644 --- a/api/current.txt +++ b/api/current.txt @@ -48759,12 +48759,8 @@ package android.telephony.ims { method public void onCapabilitiesStatusChanged(@NonNull android.telephony.ims.feature.MmTelFeature.MmTelCapabilities); } - public class ImsRcsManager implements android.telephony.ims.RegistrationManager { - method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public void getRegistrationState(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public void getRegistrationTransportType(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + public class ImsRcsManager { method @NonNull public android.telephony.ims.RcsUceAdapter getUceAdapter(); - method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public void registerImsRegistrationCallback(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.RegistrationManager.RegistrationCallback) throws android.telephony.ims.ImsException; - method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public void unregisterImsRegistrationCallback(@NonNull android.telephony.ims.RegistrationManager.RegistrationCallback); field public static final String ACTION_SHOW_CAPABILITY_DISCOVERY_OPT_IN = "android.telephony.ims.action.SHOW_CAPABILITY_DISCOVERY_OPT_IN"; } diff --git a/api/test-current.txt b/api/test-current.txt index 755380e93b2a..ca291f373be2 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -126,6 +126,7 @@ package android.app { public class ActivityTaskManager { method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) public void clearLaunchParamsForPackages(java.util.List<java.lang.String>); + method public static boolean currentUiModeSupportsErrorDialogs(@NonNull android.content.Context); method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) public String listAllStacks(); method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) public void moveTaskToStack(int, int, boolean); method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) public boolean moveTopActivityToPinnedStack(int, android.graphics.Rect); @@ -504,6 +505,11 @@ package android.app { method public boolean isStatusBarExpansionDisabled(); } + public class TaskInfo { + method @NonNull public android.content.res.Configuration getConfiguration(); + method @NonNull public android.window.WindowContainerToken getToken(); + } + public class TimePickerDialog extends android.app.AlertDialog implements android.content.DialogInterface.OnClickListener android.widget.TimePicker.OnTimeChangedListener { method public android.widget.TimePicker getTimePicker(); } diff --git a/cmds/screencap/screencap.cpp b/cmds/screencap/screencap.cpp index 4410f1c4570c..bb32dd2fa7ad 100644 --- a/cmds/screencap/screencap.cpp +++ b/cmds/screencap/screencap.cpp @@ -105,7 +105,7 @@ static status_t notifyMediaScanner(const char* fileName) { char *cmd[] = { (char*) "am", (char*) "broadcast", - (char*) "am", + (char*) "-a", (char*) "android.intent.action.MEDIA_SCANNER_SCAN_FILE", (char*) "-d", &filePath[0], diff --git a/cmds/statsd/Android.bp b/cmds/statsd/Android.bp index acd9ec3be210..0e49d187a3b5 100644 --- a/cmds/statsd/Android.bp +++ b/cmds/statsd/Android.bp @@ -104,7 +104,7 @@ cc_defaults { "src/subscriber/IncidentdReporter.cpp", "src/subscriber/SubscriberReporter.cpp", "src/uid_data.proto", - "src/utils/NamedLatch.cpp", + "src/utils/MultiConditionTrigger.cpp", ], local_include_dirs: [ @@ -366,7 +366,7 @@ cc_test { "tests/StatsService_test.cpp", "tests/storage/StorageManager_test.cpp", "tests/UidMap_test.cpp", - "tests/utils/NamedLatch_test.cpp", + "tests/utils/MultiConditionTrigger_test.cpp", ], static_libs: [ diff --git a/cmds/statsd/src/FieldValue.h b/cmds/statsd/src/FieldValue.h index e251399776fb..ba4cf11b84f1 100644 --- a/cmds/statsd/src/FieldValue.h +++ b/cmds/statsd/src/FieldValue.h @@ -382,10 +382,6 @@ public: inline void setUidField(bool isUid) { setBitmaskAtPos(UID_POS, isUid); } - inline void setResetState(int32_t resetState) { - mResetState = resetState; - } - // Default value = false inline bool isNested() const { return getValueFromBitmask(NESTED_POS); } @@ -398,12 +394,6 @@ public: // Default value = false inline bool isUidField() const { return getValueFromBitmask(UID_POS); } - // If a reset state is not sent in the StatsEvent, returns -1. Note that a - // reset satate is only sent if and only if a reset should be triggered. - inline int32_t getResetState() const { - return mResetState; - } - private: inline void setBitmaskAtPos(int pos, bool value) { mBooleanBitmask &= ~(1 << pos); // clear @@ -417,8 +407,6 @@ private: // This is a bitmask over all annotations stored in boolean form. Because // there are only 4 booleans, just one byte is required. uint8_t mBooleanBitmask = 0; - - int32_t mResetState = -1; }; /** diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp index d914ab2436c7..cc48d50ebd50 100644 --- a/cmds/statsd/src/StatsLogProcessor.cpp +++ b/cmds/statsd/src/StatsLogProcessor.cpp @@ -389,15 +389,24 @@ void StatsLogProcessor::OnLogEvent(LogEvent* event) { void StatsLogProcessor::OnLogEvent(LogEvent* event, int64_t elapsedRealtimeNs) { std::lock_guard<std::mutex> lock(mMetricsMutex); + // Tell StatsdStats about new event + const int64_t eventElapsedTimeNs = event->GetElapsedTimestampNs(); + int atomId = event->GetTagId(); + StatsdStats::getInstance().noteAtomLogged(atomId, eventElapsedTimeNs / NS_PER_SEC); + if (!event->isValid()) { + StatsdStats::getInstance().noteAtomError(atomId); + return; + } + // Hard-coded logic to update train info on disk and fill in any information // this log event may be missing. - if (event->GetTagId() == android::os::statsd::util::BINARY_PUSH_STATE_CHANGED) { + if (atomId == android::os::statsd::util::BINARY_PUSH_STATE_CHANGED) { onBinaryPushStateChangedEventLocked(event); } // Hard-coded logic to update experiment ids on disk for certain rollback // types and fill the rollback atom with experiment ids - if (event->GetTagId() == android::os::statsd::util::WATCHDOG_ROLLBACK_OCCURRED) { + if (atomId == android::os::statsd::util::WATCHDOG_ROLLBACK_OCCURRED) { onWatchdogRollbackOccurredLocked(event); } @@ -406,16 +415,11 @@ void StatsLogProcessor::OnLogEvent(LogEvent* event, int64_t elapsedRealtimeNs) { ALOGI("%s", event->ToString().c_str()); } #endif - const int64_t eventElapsedTimeNs = event->GetElapsedTimestampNs(); - resetIfConfigTtlExpiredLocked(eventElapsedTimeNs); - StatsdStats::getInstance().noteAtomLogged( - event->GetTagId(), event->GetElapsedTimestampNs() / NS_PER_SEC); - // Hard-coded logic to update the isolated uid's in the uid-map. // The field numbers need to be currently updated by hand with atoms.proto - if (event->GetTagId() == android::os::statsd::util::ISOLATED_UID_CHANGED) { + if (atomId == android::os::statsd::util::ISOLATED_UID_CHANGED) { onIsolatedUidChangedEventLocked(*event); } @@ -432,7 +436,7 @@ void StatsLogProcessor::OnLogEvent(LogEvent* event, int64_t elapsedRealtimeNs) { } - if (event->GetTagId() != android::os::statsd::util::ISOLATED_UID_CHANGED) { + if (atomId != android::os::statsd::util::ISOLATED_UID_CHANGED) { // Map the isolated uid to host uid if necessary. mapIsolatedUidToHostUidIfNecessaryLocked(event); } @@ -1051,8 +1055,8 @@ int64_t StatsLogProcessor::getLastReportTimeNs(const ConfigKey& key) { void StatsLogProcessor::notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid, const int64_t version) { std::lock_guard<std::mutex> lock(mMetricsMutex); - ALOGW("Received app upgrade"); - for (auto it : mMetricsManagers) { + VLOG("Received app upgrade"); + for (const auto& it : mMetricsManagers) { it.second->notifyAppUpgrade(eventTimeNs, apk, uid, version); } } @@ -1060,20 +1064,28 @@ void StatsLogProcessor::notifyAppUpgrade(const int64_t& eventTimeNs, const strin void StatsLogProcessor::notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) { std::lock_guard<std::mutex> lock(mMetricsMutex); - ALOGW("Received app removed"); - for (auto it : mMetricsManagers) { + VLOG("Received app removed"); + for (const auto& it : mMetricsManagers) { it.second->notifyAppRemoved(eventTimeNs, apk, uid); } } void StatsLogProcessor::onUidMapReceived(const int64_t& eventTimeNs) { std::lock_guard<std::mutex> lock(mMetricsMutex); - ALOGW("Received uid map"); - for (auto it : mMetricsManagers) { + VLOG("Received uid map"); + for (const auto& it : mMetricsManagers) { it.second->onUidMapReceived(eventTimeNs); } } +void StatsLogProcessor::onStatsdInitCompleted(const int64_t& elapsedTimeNs) { + std::lock_guard<std::mutex> lock(mMetricsMutex); + VLOG("Received boot completed signal"); + for (const auto& it : mMetricsManagers) { + it.second->onStatsdInitCompleted(elapsedTimeNs); + } +} + void StatsLogProcessor::noteOnDiskData(const ConfigKey& key) { std::lock_guard<std::mutex> lock(mMetricsMutex); mOnDiskDataConfigs.insert(key); diff --git a/cmds/statsd/src/StatsLogProcessor.h b/cmds/statsd/src/StatsLogProcessor.h index 97512ed7595c..ffd83ba978f4 100644 --- a/cmds/statsd/src/StatsLogProcessor.h +++ b/cmds/statsd/src/StatsLogProcessor.h @@ -120,6 +120,11 @@ public: /* Notify all MetricsManagers of uid map snapshots received */ void onUidMapReceived(const int64_t& eventTimeNs) override; + /* Notify all metrics managers of boot completed + * This will force a bucket split when the boot is finished. + */ + void onStatsdInitCompleted(const int64_t& elapsedTimeNs); + // Reset all configs. void resetConfigs(); diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp index ae7a8d0d30cc..bd9f7a59fcbd 100644 --- a/cmds/statsd/src/StatsService.cpp +++ b/cmds/statsd/src/StatsService.cpp @@ -118,7 +118,8 @@ StatsService::StatsService(const sp<Looper>& handlerLooper, shared_ptr<LogEventQ } })), mEventQueue(queue), - mBootCompleteLatch({kBootCompleteTag, kUidMapReceivedTag, kAllPullersRegisteredTag}), + mBootCompleteTrigger({kBootCompleteTag, kUidMapReceivedTag, kAllPullersRegisteredTag}, + [this]() { mProcessor->onStatsdInitCompleted(getElapsedRealtimeNs()); }), mStatsCompanionServiceDeathRecipient( AIBinder_DeathRecipient_new(StatsService::statsCompanionServiceDied)) { mUidMap = UidMap::getInstance(); @@ -165,12 +166,6 @@ StatsService::StatsService(const sp<Looper>& handlerLooper, shared_ptr<LogEventQ std::thread pushedEventThread([this] { readLogs(); }); pushedEventThread.detach(); } - - std::thread bootCompletedThread([this] { - mBootCompleteLatch.wait(); - VLOG("In the boot completed thread"); - }); - bootCompletedThread.detach(); } StatsService::~StatsService() { @@ -946,7 +941,7 @@ Status StatsService::informAllUidData(const ScopedFileDescriptor& fd) { packageNames, installers); - mBootCompleteLatch.countDown(kUidMapReceivedTag); + mBootCompleteTrigger.markComplete(kUidMapReceivedTag); VLOG("StatsService::informAllUidData UidData proto parsed successfully."); return Status::ok(); } @@ -1066,7 +1061,7 @@ Status StatsService::bootCompleted() { ENFORCE_UID(AID_SYSTEM); VLOG("StatsService::bootCompleted was called"); - mBootCompleteLatch.countDown(kBootCompleteTag); + mBootCompleteTrigger.markComplete(kBootCompleteTag); return Status::ok(); } @@ -1222,20 +1217,11 @@ Status StatsService::unsetBroadcastSubscriber(int64_t configId, return Status::ok(); } -Status StatsService::sendAppBreadcrumbAtom(int32_t label, int32_t state) { - // Permission check not necessary as it's meant for applications to write to - // statsd. - android::os::statsd::util::stats_write(android::os::statsd::util::APP_BREADCRUMB_REPORTED, - (int32_t) AIBinder_getCallingUid(), label, - state); - return Status::ok(); -} - Status StatsService::allPullersFromBootRegistered() { ENFORCE_UID(AID_SYSTEM); VLOG("StatsService::allPullersFromBootRegistered was called"); - mBootCompleteLatch.countDown(kAllPullersRegisteredTag); + mBootCompleteTrigger.markComplete(kAllPullersRegisteredTag); return Status::ok(); } diff --git a/cmds/statsd/src/StatsService.h b/cmds/statsd/src/StatsService.h index 79324d89d8e8..b49fa1d42e66 100644 --- a/cmds/statsd/src/StatsService.h +++ b/cmds/statsd/src/StatsService.h @@ -33,7 +33,7 @@ #include "packages/UidMap.h" #include "shell/ShellSubscriber.h" #include "statscompanion_util.h" -#include "utils/NamedLatch.h" +#include "utils/MultiConditionTrigger.h" using namespace android; using namespace android::os; @@ -162,11 +162,6 @@ public: virtual void sayHiToStatsCompanion(); /** - * Binder call to get AppBreadcrumbReported atom. - */ - virtual Status sendAppBreadcrumbAtom(int32_t label, int32_t state) override; - - /** * Binder call to notify statsd that all pullers from boot have been registered. */ virtual Status allPullersFromBootRegistered(); @@ -386,7 +381,7 @@ private: mutable mutex mShellSubscriberMutex; std::shared_ptr<LogEventQueue> mEventQueue; - NamedLatch mBootCompleteLatch; + MultiConditionTrigger mBootCompleteTrigger; static const inline string kBootCompleteTag = "BOOT_COMPLETE"; static const inline string kUidMapReceivedTag = "UID_MAP"; static const inline string kAllPullersRegisteredTag = "PULLERS_REGISTERED"; @@ -399,11 +394,14 @@ private: FRIEND_TEST(StatsServiceTest, TestAddConfig_invalid); FRIEND_TEST(StatsServiceTest, TestGetUidFromArgs); FRIEND_TEST(PartialBucketE2eTest, TestCountMetricNoSplitOnNewApp); + FRIEND_TEST(PartialBucketE2eTest, TestCountMetricSplitOnBoot); FRIEND_TEST(PartialBucketE2eTest, TestCountMetricSplitOnUpgrade); FRIEND_TEST(PartialBucketE2eTest, TestCountMetricSplitOnRemoval); FRIEND_TEST(PartialBucketE2eTest, TestCountMetricWithoutSplit); + FRIEND_TEST(PartialBucketE2eTest, TestValueMetricOnBootWithoutMinPartialBucket); FRIEND_TEST(PartialBucketE2eTest, TestValueMetricWithoutMinPartialBucket); FRIEND_TEST(PartialBucketE2eTest, TestValueMetricWithMinPartialBucket); + FRIEND_TEST(PartialBucketE2eTest, TestGaugeMetricOnBootWithoutMinPartialBucket); FRIEND_TEST(PartialBucketE2eTest, TestGaugeMetricWithoutMinPartialBucket); FRIEND_TEST(PartialBucketE2eTest, TestGaugeMetricWithMinPartialBucket); }; diff --git a/cmds/statsd/src/external/StatsCallbackPuller.cpp b/cmds/statsd/src/external/StatsCallbackPuller.cpp index 933f48d1714b..3618bb0dd08b 100644 --- a/cmds/statsd/src/external/StatsCallbackPuller.cpp +++ b/cmds/statsd/src/external/StatsCallbackPuller.cpp @@ -67,8 +67,14 @@ bool StatsCallbackPuller::PullInternal(vector<shared_ptr<LogEvent>>* data) { lock_guard<mutex> lk(*cv_mutex); for (const StatsEventParcel& parcel: output) { shared_ptr<LogEvent> event = make_shared<LogEvent>(/*uid=*/-1, /*pid=*/-1); - event->parseBuffer((uint8_t*)parcel.buffer.data(), parcel.buffer.size()); - sharedData->push_back(event); + bool valid = event->parseBuffer((uint8_t*)parcel.buffer.data(), + parcel.buffer.size()); + if (valid) { + sharedData->push_back(event); + } else { + StatsdStats::getInstance().noteAtomError(event->GetTagId(), + /*pull=*/true); + } } *pullSuccess = success; *pullFinish = true; diff --git a/cmds/statsd/src/guardrail/StatsdStats.cpp b/cmds/statsd/src/guardrail/StatsdStats.cpp index db637b15e969..46f5dbda5521 100644 --- a/cmds/statsd/src/guardrail/StatsdStats.cpp +++ b/cmds/statsd/src/guardrail/StatsdStats.cpp @@ -54,6 +54,7 @@ const int FIELD_ID_ACTIVATION_BROADCAST_GUARDRAIL = 19; const int FIELD_ID_ATOM_STATS_TAG = 1; const int FIELD_ID_ATOM_STATS_COUNT = 2; +const int FIELD_ID_ATOM_STATS_ERROR_COUNT = 3; const int FIELD_ID_ANOMALY_ALARMS_REGISTERED = 1; const int FIELD_ID_PERIODIC_ALARMS_REGISTERED = 1; @@ -549,6 +550,20 @@ void StatsdStats::noteBucketBoundaryDelayNs(int64_t metricId, int64_t timeDelayN std::min(pullStats.minBucketBoundaryDelayNs, timeDelayNs); } +void StatsdStats::noteAtomError(int atomTag, bool pull) { + lock_guard<std::mutex> lock(mLock); + if (pull) { + mPulledAtomStats[atomTag].atomErrorCount++; + return; + } + + bool present = (mPushedAtomErrorStats.find(atomTag) != mPushedAtomErrorStats.end()); + bool full = (mPushedAtomErrorStats.size() >= (size_t)kMaxPushedAtomErrorStatsSize); + if (!full || present) { + mPushedAtomErrorStats[atomTag]++; + } +} + StatsdStats::AtomMetricStats& StatsdStats::getAtomMetricStats(int64_t metricId) { auto atomMetricStatsIter = mAtomMetricStats.find(metricId); if (atomMetricStatsIter != mAtomMetricStats.end()) { @@ -604,9 +619,11 @@ void StatsdStats::resetInternalLocked() { pullStats.second.pullExceedMaxDelay = 0; pullStats.second.registeredCount = 0; pullStats.second.unregisteredCount = 0; + pullStats.second.atomErrorCount = 0; } mAtomMetricStats.clear(); mActivationBroadcastGuardrailStats.clear(); + mPushedAtomErrorStats.clear(); } string buildTimeString(int64_t timeSec) { @@ -617,6 +634,15 @@ string buildTimeString(int64_t timeSec) { return string(timeBuffer); } +int StatsdStats::getPushedAtomErrors(int atomId) const { + const auto& it = mPushedAtomErrorStats.find(atomId); + if (it != mPushedAtomErrorStats.end()) { + return it->second; + } else { + return 0; + } +} + void StatsdStats::dumpStats(int out) const { lock_guard<std::mutex> lock(mLock); time_t t = mStartTimeSec; @@ -721,11 +747,13 @@ void StatsdStats::dumpStats(int out) const { const size_t atomCounts = mPushedAtomStats.size(); for (size_t i = 2; i < atomCounts; i++) { if (mPushedAtomStats[i] > 0) { - dprintf(out, "Atom %lu->%d\n", (unsigned long)i, mPushedAtomStats[i]); + dprintf(out, "Atom %zu->(total count)%d, (error count)%d\n", i, mPushedAtomStats[i], + getPushedAtomErrors((int)i)); } } for (const auto& pair : mNonPlatformPushedAtomStats) { - dprintf(out, "Atom %lu->%d\n", (unsigned long)pair.first, pair.second); + dprintf(out, "Atom %d->(total count)%d, (error count)%d\n", pair.first, pair.second, + getPushedAtomErrors(pair.first)); } dprintf(out, "********Pulled Atom stats***********\n"); @@ -737,13 +765,15 @@ void StatsdStats::dumpStats(int out) const { "nanos)%lld, " " (max pull delay nanos)%lld, (data error)%ld\n" " (pull timeout)%ld, (pull exceed max delay)%ld\n" - " (registered count) %ld, (unregistered count) %ld\n", + " (registered count) %ld, (unregistered count) %ld\n" + " (atom error count) %d\n", (int)pair.first, (long)pair.second.totalPull, (long)pair.second.totalPullFromCache, (long)pair.second.pullFailed, (long)pair.second.minPullIntervalSec, (long long)pair.second.avgPullTimeNs, (long long)pair.second.maxPullTimeNs, (long long)pair.second.avgPullDelayNs, (long long)pair.second.maxPullDelayNs, pair.second.dataError, pair.second.pullTimeout, pair.second.pullExceedMaxDelay, - pair.second.registeredCount, pair.second.unregisteredCount); + pair.second.registeredCount, pair.second.unregisteredCount, + pair.second.atomErrorCount); } if (mAnomalyAlarmRegisteredStats > 0) { @@ -919,6 +949,10 @@ void StatsdStats::dumpStats(std::vector<uint8_t>* output, bool reset) { proto.start(FIELD_TYPE_MESSAGE | FIELD_ID_ATOM_STATS | FIELD_COUNT_REPEATED); proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_TAG, (int32_t)i); proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_COUNT, mPushedAtomStats[i]); + int errors = getPushedAtomErrors(i); + if (errors > 0) { + proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_ERROR_COUNT, errors); + } proto.end(token); } } @@ -928,6 +962,10 @@ void StatsdStats::dumpStats(std::vector<uint8_t>* output, bool reset) { proto.start(FIELD_TYPE_MESSAGE | FIELD_ID_ATOM_STATS | FIELD_COUNT_REPEATED); proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_TAG, pair.first); proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_COUNT, pair.second); + int errors = getPushedAtomErrors(pair.first); + if (errors > 0) { + proto.write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_STATS_ERROR_COUNT, errors); + } proto.end(token); } diff --git a/cmds/statsd/src/guardrail/StatsdStats.h b/cmds/statsd/src/guardrail/StatsdStats.h index ff31e9e73fd0..21e524a9fadf 100644 --- a/cmds/statsd/src/guardrail/StatsdStats.h +++ b/cmds/statsd/src/guardrail/StatsdStats.h @@ -461,6 +461,16 @@ public: */ void noteActivationBroadcastGuardrailHit(const int uid); + /** + * Reports that an atom is erroneous or cannot be parsed successfully by + * statsd. An atom tag of 0 indicates that the client did not supply the + * atom id within the encoding. + * + * For pushed atoms only, this call should be preceded by a call to + * noteAtomLogged. + */ + void noteAtomError(int atomTag, bool pull=false); + /** * Reset the historical stats. Including all stats in icebox, and the tracked stats about * metrics, matchers, and atoms. The active configs will be kept and StatsdStats will continue @@ -499,6 +509,7 @@ public: long emptyData = 0; long registeredCount = 0; long unregisteredCount = 0; + int32_t atomErrorCount = 0; } PulledAtomStats; typedef struct { @@ -546,6 +557,12 @@ private: // Maps PullAtomId to its stats. The size is capped by the puller atom counts. std::map<int, PulledAtomStats> mPulledAtomStats; + // Stores the number of times a pushed atom was logged erroneously. The + // corresponding counts for pulled atoms are stored in PulledAtomStats. + // The max size of this map is kMaxAtomErrorsStatsSize. + std::map<int, int> mPushedAtomErrorStats; + int kMaxPushedAtomErrorStatsSize = 100; + // Maps metric ID to its stats. The size is capped by the number of metrics. std::map<int64_t, AtomMetricStats> mAtomMetricStats; @@ -613,6 +630,8 @@ private: void addToIceBoxLocked(std::shared_ptr<ConfigStats>& stats); + int getPushedAtomErrors(int atomId) const; + /** * Get a reference to AtomMetricStats for a metric. If none exists, create it. The reference * will live as long as `this`. @@ -631,6 +650,7 @@ private: FRIEND_TEST(StatsdStatsTest, TestPullAtomStats); FRIEND_TEST(StatsdStatsTest, TestAtomMetricsStats); FRIEND_TEST(StatsdStatsTest, TestActivationBroadcastGuardrailHit); + FRIEND_TEST(StatsdStatsTest, TestAtomErrorStats); }; } // namespace statsd diff --git a/cmds/statsd/src/logd/LogEvent.cpp b/cmds/statsd/src/logd/LogEvent.cpp index 61cd01728ab1..eb830e114b40 100644 --- a/cmds/statsd/src/logd/LogEvent.cpp +++ b/cmds/statsd/src/logd/LogEvent.cpp @@ -114,14 +114,6 @@ LogEvent::LogEvent(int64_t wallClockTimestampNs, int64_t elapsedTimestampNs, mValues.push_back(FieldValue(Field(mTagId, getSimpleField(4)), Value(trainInfo.status))); } -LogEvent::~LogEvent() { - if (mContext) { - // This is for the case when LogEvent is created using the test interface - // but init() isn't called. - android_log_destroy(&mContext); - } -} - void LogEvent::parseInt32(int32_t* pos, int32_t depth, bool* last, uint8_t numAnnotations) { int32_t value = readNextValue<int32_t>(); addToValues(pos, depth, value, last); @@ -303,8 +295,7 @@ void LogEvent::parseTriggerStateResetAnnotation(uint8_t annotationType) { return; } - int32_t resetState = readNextValue<int32_t>(); - mValues[mValues.size() - 1].mAnnotations.setResetState(resetState); + mResetState = readNextValue<int32_t>(); } void LogEvent::parseStateNestedAnnotation(uint8_t annotationType) { @@ -386,7 +377,6 @@ bool LogEvent::parseBuffer(uint8_t* buf, size_t len) { typeInfo = readNextValue<uint8_t>(); uint8_t typeId = getTypeId(typeInfo); - // TODO(b/144373276): handle errors passed to the socket switch (typeId) { case BOOL_TYPE: parseBool(pos, /*depth=*/0, last, getNumAnnotations(typeInfo)); @@ -413,8 +403,13 @@ bool LogEvent::parseBuffer(uint8_t* buf, size_t len) { parseAttributionChain(pos, /*depth=*/0, last, getNumAnnotations(typeInfo)); if (mAttributionChainIndex == -1) mAttributionChainIndex = pos[0]; break; + case ERROR_TYPE: + mErrorBitmask = readNextValue<int32_t>(); + mValid = false; + break; default: mValid = false; + break; } } diff --git a/cmds/statsd/src/logd/LogEvent.h b/cmds/statsd/src/logd/LogEvent.h index 41fdcc2cbe7a..dedcfaf6cd87 100644 --- a/cmds/statsd/src/logd/LogEvent.h +++ b/cmds/statsd/src/logd/LogEvent.h @@ -70,7 +70,7 @@ public: explicit LogEvent(int64_t wallClockTimestampNs, int64_t elapsedTimestampNs, const InstallTrainInfo& installTrainInfo); - ~LogEvent(); + ~LogEvent() {} /** * Get the timestamp associated with this event. @@ -184,6 +184,12 @@ public: return mExclusiveStateFieldIndex; } + // If a reset state is not sent in the StatsEvent, returns -1. Note that a + // reset state is sent if and only if a reset should be triggered. + inline int getResetState() const { + return mResetState; + } + inline LogEvent makeCopy() { return LogEvent(*this); } @@ -204,6 +210,14 @@ public: return BAD_INDEX; } + bool isValid() const { + return mValid; + } + + int32_t getErrorBitmask() const { + return mErrorBitmask; + } + private: /** * Only use this if copy is absolutely needed. @@ -230,12 +244,13 @@ private: bool checkPreviousValueType(Type expected); /** - * The below three variables are only valid during the execution of + * The below two variables are only valid during the execution of * parseBuffer. There are no guarantees about the state of these variables * before/after. */ uint8_t* mBuf; uint32_t mRemainingLen; // number of valid bytes left in the buffer being parsed + bool mValid = true; // stores whether the event we received from the socket is valid /** @@ -287,19 +302,15 @@ private: // matching. std::vector<FieldValue> mValues; - // This field is used when statsD wants to create log event object and write fields to it. After - // calling init() function, this object would be destroyed to save memory usage. - // When the log event is created from log msg, this field is never initiated. - android_log_context mContext = NULL; - // The timestamp set by the logd. int64_t mLogdTimestampNs; // The elapsed timestamp set by statsd log writer. int64_t mElapsedTimestampNs; - // The atom tag of the event. - int mTagId; + // The atom tag of the event (defaults to 0 if client does not + // appropriately set the atom id). + int mTagId = 0; // The uid of the logging client (defaults to -1). int32_t mLogUid = -1; @@ -307,11 +318,15 @@ private: // The pid of the logging client (defaults to -1). int32_t mLogPid = -1; + // Bitmask of errors sent by StatsEvent/AStatsEvent. + int32_t mErrorBitmask = 0; + // Annotations bool mTruncateTimestamp = false; int mUidFieldIndex = -1; int mAttributionChainIndex = -1; int mExclusiveStateFieldIndex = -1; + int mResetState = -1; }; void writeExperimentIdsToProto(const std::vector<int64_t>& experimentIds, std::vector<uint8_t>* protoOut); diff --git a/cmds/statsd/src/main.cpp b/cmds/statsd/src/main.cpp index e3945334aeca..cd9c4e5b947b 100644 --- a/cmds/statsd/src/main.cpp +++ b/cmds/statsd/src/main.cpp @@ -37,6 +37,7 @@ using std::shared_ptr; using std::make_shared; shared_ptr<StatsService> gStatsService = nullptr; +sp<StatsSocketListener> gSocketListener = nullptr; void signalHandler(int sig) { if (sig == SIGPIPE) { @@ -47,6 +48,7 @@ void signalHandler(int sig) { return; } + if (gSocketListener != nullptr) gSocketListener->stopListener(); if (gStatsService != nullptr) gStatsService->Terminate(); ALOGW("statsd terminated on receiving signal %d.", sig); exit(1); @@ -92,11 +94,11 @@ int main(int /*argc*/, char** /*argv*/) { gStatsService->Startup(); - sp<StatsSocketListener> socketListener = new StatsSocketListener(eventQueue); + gSocketListener = new StatsSocketListener(eventQueue); ALOGI("Statsd starts to listen to socket."); // Backlog and /proc/sys/net/unix/max_dgram_qlen set to large value - if (socketListener->startListener(600)) { + if (gSocketListener->startListener(600)) { exit(1); } diff --git a/cmds/statsd/src/metrics/CountMetricProducer.h b/cmds/statsd/src/metrics/CountMetricProducer.h index a4711e8357f2..f9a8842efc3d 100644 --- a/cmds/statsd/src/metrics/CountMetricProducer.h +++ b/cmds/statsd/src/metrics/CountMetricProducer.h @@ -109,10 +109,11 @@ private: FRIEND_TEST(CountMetricProducerTest, TestEventsWithNonSlicedCondition); FRIEND_TEST(CountMetricProducerTest, TestEventsWithSlicedCondition); FRIEND_TEST(CountMetricProducerTest, TestAnomalyDetectionUnSliced); - FRIEND_TEST(CountMetricProducerTest, TestEventWithAppUpgrade); - FRIEND_TEST(CountMetricProducerTest, TestEventWithAppUpgradeInNextBucket); FRIEND_TEST(CountMetricProducerTest, TestFirstBucket); FRIEND_TEST(CountMetricProducerTest, TestOneWeekTimeUnit); + + FRIEND_TEST(CountMetricProducerTest_PartialBucket, TestSplitInCurrentBucket); + FRIEND_TEST(CountMetricProducerTest_PartialBucket, TestSplitInNextBucket); }; } // namespace statsd diff --git a/cmds/statsd/src/metrics/DurationMetricProducer.h b/cmds/statsd/src/metrics/DurationMetricProducer.h index cc48f99add01..6f84076ee6b5 100644 --- a/cmds/statsd/src/metrics/DurationMetricProducer.h +++ b/cmds/statsd/src/metrics/DurationMetricProducer.h @@ -154,12 +154,14 @@ private: FRIEND_TEST(DurationMetricTrackerTest, TestNoCondition); FRIEND_TEST(DurationMetricTrackerTest, TestNonSlicedCondition); FRIEND_TEST(DurationMetricTrackerTest, TestNonSlicedConditionUnknownState); - FRIEND_TEST(DurationMetricTrackerTest, TestSumDurationWithUpgrade); - FRIEND_TEST(DurationMetricTrackerTest, TestSumDurationWithUpgradeInFollowingBucket); - FRIEND_TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgrade); - FRIEND_TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgradeInNextBucket); FRIEND_TEST(WakelockDurationE2eTest, TestAggregatedPredicates); FRIEND_TEST(DurationMetricTrackerTest, TestFirstBucket); + + FRIEND_TEST(DurationMetricProducerTest_PartialBucket, TestSumDuration); + FRIEND_TEST(DurationMetricProducerTest_PartialBucket, + TestSumDurationWithSplitInFollowingBucket); + FRIEND_TEST(DurationMetricProducerTest_PartialBucket, TestMaxDuration); + FRIEND_TEST(DurationMetricProducerTest_PartialBucket, TestMaxDurationWithSplitInNextBucket); }; } // namespace statsd diff --git a/cmds/statsd/src/metrics/GaugeMetricProducer.h b/cmds/statsd/src/metrics/GaugeMetricProducer.h index aa0cae26080d..2eb584b097ea 100644 --- a/cmds/statsd/src/metrics/GaugeMetricProducer.h +++ b/cmds/statsd/src/metrics/GaugeMetricProducer.h @@ -73,18 +73,23 @@ public: bool pullSuccess, int64_t originalPullTimeNs) override; // GaugeMetric needs to immediately trigger another pull when we create the partial bucket. - void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid, - const int64_t version) override { + void notifyAppUpgrade(const int64_t& eventTimeNs) override { std::lock_guard<std::mutex> lock(mMutex); if (!mSplitBucketForAppUpgrade) { return; } - if (eventTimeNs > getCurrentBucketEndTimeNs()) { - // Flush full buckets on the normal path up to the latest bucket boundary. - flushIfNeededLocked(eventTimeNs); + flushLocked(eventTimeNs); + if (mIsPulled && mSamplingType == GaugeMetric::RANDOM_ONE_SAMPLE) { + pullAndMatchEventsLocked(eventTimeNs); } - flushCurrentBucketLocked(eventTimeNs, eventTimeNs); + }; + + // GaugeMetric needs to immediately trigger another pull when we create the partial bucket. + void onStatsdInitCompleted(const int64_t& eventTimeNs) override { + std::lock_guard<std::mutex> lock(mMutex); + + flushLocked(eventTimeNs); if (mIsPulled && mSamplingType == GaugeMetric::RANDOM_ONE_SAMPLE) { pullAndMatchEventsLocked(eventTimeNs); } @@ -190,13 +195,14 @@ private: FRIEND_TEST(GaugeMetricProducerTest, TestPulledEventsWithCondition); FRIEND_TEST(GaugeMetricProducerTest, TestPulledEventsWithSlicedCondition); FRIEND_TEST(GaugeMetricProducerTest, TestPulledEventsNoCondition); - FRIEND_TEST(GaugeMetricProducerTest, TestPushedEventsWithUpgrade); - FRIEND_TEST(GaugeMetricProducerTest, TestPulledWithUpgrade); FRIEND_TEST(GaugeMetricProducerTest, TestPulledWithAppUpgradeDisabled); FRIEND_TEST(GaugeMetricProducerTest, TestPulledEventsAnomalyDetection); FRIEND_TEST(GaugeMetricProducerTest, TestFirstBucket); FRIEND_TEST(GaugeMetricProducerTest, TestPullOnTrigger); FRIEND_TEST(GaugeMetricProducerTest, TestRemoveDimensionInOutput); + + FRIEND_TEST(GaugeMetricProducerTest_PartialBucket, TestPushedEvents); + FRIEND_TEST(GaugeMetricProducerTest_PartialBucket, TestPulled); }; } // namespace statsd diff --git a/cmds/statsd/src/metrics/MetricProducer.h b/cmds/statsd/src/metrics/MetricProducer.h index 6aba13ca7859..91c98ea27269 100644 --- a/cmds/statsd/src/metrics/MetricProducer.h +++ b/cmds/statsd/src/metrics/MetricProducer.h @@ -141,30 +141,25 @@ public: } /** - * Forces this metric to split into a partial bucket right now. If we're past a full bucket, we - * first call the standard flushing code to flush up to the latest full bucket. Then we call - * the flush again when the end timestamp is forced to be now, and then after flushing, update - * the start timestamp to be now. + * Force a partial bucket split on app upgrade */ - virtual void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid, - const int64_t version) { + virtual void notifyAppUpgrade(const int64_t& eventTimeNs) { std::lock_guard<std::mutex> lock(mMutex); - - if (eventTimeNs > getCurrentBucketEndTimeNs()) { - // Flush full buckets on the normal path up to the latest bucket boundary. - flushIfNeededLocked(eventTimeNs); - } - // Now flush a partial bucket. - flushCurrentBucketLocked(eventTimeNs, eventTimeNs); - // Don't update the current bucket number so that the anomaly tracker knows this bucket - // is a partial bucket and can merge it with the previous bucket. + flushLocked(eventTimeNs); }; - void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) { + void notifyAppRemoved(const int64_t& eventTimeNs) { // Force buckets to split on removal also. - notifyAppUpgrade(eventTimeNs, apk, uid, 0); + notifyAppUpgrade(eventTimeNs); }; + /** + * Force a partial bucket split on boot complete. + */ + virtual void onStatsdInitCompleted(const int64_t& eventTimeNs) { + std::lock_guard<std::mutex> lock(mMutex); + flushLocked(eventTimeNs); + } // Consume the parsed stats log entry that already matched the "what" of the metric. void onMatchedLogEvent(const size_t matcherIndex, const LogEvent& event) { std::lock_guard<std::mutex> lock(mMutex); @@ -292,8 +287,7 @@ public: // End: getters/setters protected: /** - * Flushes the current bucket if the eventTime is after the current bucket's end time. This will - also flush the current partial bucket in memory. + * Flushes the current bucket if the eventTime is after the current bucket's end time. */ virtual void flushIfNeededLocked(const int64_t& eventTime){}; diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp index d832ed86580d..bcca1fd9fdc9 100644 --- a/cmds/statsd/src/metrics/MetricsManager.cpp +++ b/cmds/statsd/src/metrics/MetricsManager.cpp @@ -231,8 +231,8 @@ bool MetricsManager::isConfigValid() const { void MetricsManager::notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid, const int64_t version) { // Inform all metric producers. - for (auto it : mAllMetricProducers) { - it->notifyAppUpgrade(eventTimeNs, apk, uid, version); + for (const auto& it : mAllMetricProducers) { + it->notifyAppUpgrade(eventTimeNs); } // check if we care this package if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) != mAllowedPkg.end()) { @@ -252,8 +252,8 @@ void MetricsManager::notifyAppUpgrade(const int64_t& eventTimeNs, const string& void MetricsManager::notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) { // Inform all metric producers. - for (auto it : mAllMetricProducers) { - it->notifyAppRemoved(eventTimeNs, apk, uid); + for (const auto& it : mAllMetricProducers) { + it->notifyAppRemoved(eventTimeNs); } // check if we care this package if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) != mAllowedPkg.end()) { @@ -282,6 +282,13 @@ void MetricsManager::onUidMapReceived(const int64_t& eventTimeNs) { initLogSourceWhiteList(); } +void MetricsManager::onStatsdInitCompleted(const int64_t& eventTimeNs) { + // Inform all metric producers. + for (const auto& it : mAllMetricProducers) { + it->onStatsdInitCompleted(eventTimeNs); + } +} + void MetricsManager::init() { for (const auto& producer : mAllMetricProducers) { producer->prepareFirstBucket(); diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h index 1fd6572cc760..ef03d2064ab0 100644 --- a/cmds/statsd/src/metrics/MetricsManager.h +++ b/cmds/statsd/src/metrics/MetricsManager.h @@ -70,6 +70,8 @@ public: void onUidMapReceived(const int64_t& eventTimeNs); + void onStatsdInitCompleted(const int64_t& elapsedTimeNs); + void init(); vector<int32_t> getPullAtomUids(int32_t atomId) override; diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.h b/cmds/statsd/src/metrics/ValueMetricProducer.h index e9273dc54424..c8dc8cc290c4 100644 --- a/cmds/statsd/src/metrics/ValueMetricProducer.h +++ b/cmds/statsd/src/metrics/ValueMetricProducer.h @@ -69,8 +69,7 @@ public: bool pullSuccess, int64_t originalPullTimeNs) override; // ValueMetric needs special logic if it's a pulled atom. - void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid, - const int64_t version) override { + void notifyAppUpgrade(const int64_t& eventTimeNs) override { std::lock_guard<std::mutex> lock(mMutex); if (!mSplitBucketForAppUpgrade) { return; @@ -81,6 +80,15 @@ public: flushCurrentBucketLocked(eventTimeNs, eventTimeNs); }; + // ValueMetric needs special logic if it's a pulled atom. + void onStatsdInitCompleted(const int64_t& eventTimeNs) override { + std::lock_guard<std::mutex> lock(mMutex); + if (mIsPulled && mCondition) { + pullAndMatchEventsLocked(eventTimeNs); + } + flushCurrentBucketLocked(eventTimeNs, eventTimeNs); + }; + void onStateChanged(int64_t eventTimeNs, int32_t atomId, const HashableDimensionKey& primaryKey, int oldState, int newState) override; @@ -256,7 +264,6 @@ private: FRIEND_TEST(ValueMetricProducerTest, TestAnomalyDetection); FRIEND_TEST(ValueMetricProducerTest, TestBaseSetOnConditionChange); - FRIEND_TEST(ValueMetricProducerTest, TestBucketBoundariesOnAppUpgrade); FRIEND_TEST(ValueMetricProducerTest, TestBucketBoundariesOnConditionChange); FRIEND_TEST(ValueMetricProducerTest, TestBucketBoundaryNoCondition); FRIEND_TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition); @@ -269,10 +276,8 @@ private: FRIEND_TEST(ValueMetricProducerTest, TestEmptyDataResetsBase_onDataPulled); FRIEND_TEST(ValueMetricProducerTest, TestEventsWithNonSlicedCondition); FRIEND_TEST(ValueMetricProducerTest, TestFirstBucket); - FRIEND_TEST(ValueMetricProducerTest, TestFullBucketResetWhenLastBucketInvalid); FRIEND_TEST(ValueMetricProducerTest, TestLateOnDataPulledWithDiff); FRIEND_TEST(ValueMetricProducerTest, TestLateOnDataPulledWithoutDiff); - FRIEND_TEST(ValueMetricProducerTest, TestPartialBucketCreated); FRIEND_TEST(ValueMetricProducerTest, TestPartialResetOnBucketBoundaries); FRIEND_TEST(ValueMetricProducerTest, TestPulledData_noDiff_bucketBoundaryFalse); FRIEND_TEST(ValueMetricProducerTest, TestPulledData_noDiff_bucketBoundaryTrue); @@ -283,15 +288,12 @@ private: FRIEND_TEST(ValueMetricProducerTest, TestPulledEventsTakeAbsoluteValueOnReset); FRIEND_TEST(ValueMetricProducerTest, TestPulledEventsTakeZeroOnReset); FRIEND_TEST(ValueMetricProducerTest, TestPulledEventsWithFiltering); - FRIEND_TEST(ValueMetricProducerTest, TestPulledValueWithUpgrade); - FRIEND_TEST(ValueMetricProducerTest, TestPulledValueWithUpgradeWhileConditionFalse); FRIEND_TEST(ValueMetricProducerTest, TestPulledWithAppUpgradeDisabled); FRIEND_TEST(ValueMetricProducerTest, TestPushedAggregateAvg); FRIEND_TEST(ValueMetricProducerTest, TestPushedAggregateMax); FRIEND_TEST(ValueMetricProducerTest, TestPushedAggregateMin); FRIEND_TEST(ValueMetricProducerTest, TestPushedAggregateSum); FRIEND_TEST(ValueMetricProducerTest, TestPushedEventsWithCondition); - FRIEND_TEST(ValueMetricProducerTest, TestPushedEventsWithUpgrade); FRIEND_TEST(ValueMetricProducerTest, TestPushedEventsWithoutCondition); FRIEND_TEST(ValueMetricProducerTest, TestResetBaseOnPullDelayExceeded); FRIEND_TEST(ValueMetricProducerTest, TestResetBaseOnPullFailAfterConditionChange); @@ -313,6 +315,14 @@ private: FRIEND_TEST(ValueMetricProducerTest_BucketDrop, TestInvalidBucketWhenGuardRailHit); FRIEND_TEST(ValueMetricProducerTest_BucketDrop, TestInvalidBucketWhenAccumulateEventWrongBucket); + + FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestBucketBoundariesOnPartialBucket); + FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestFullBucketResetWhenLastBucketInvalid); + FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestPartialBucketCreated); + FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestPushedEvents); + FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestPulledValue); + FRIEND_TEST(ValueMetricProducerTest_PartialBucket, TestPulledValueWhileConditionFalse); + friend class ValueMetricProducerTestHelper; }; diff --git a/cmds/statsd/src/state/StateTracker.cpp b/cmds/statsd/src/state/StateTracker.cpp index b7f314a819df..b63713b64c5d 100644 --- a/cmds/statsd/src/state/StateTracker.cpp +++ b/cmds/statsd/src/state/StateTracker.cpp @@ -51,7 +51,7 @@ void StateTracker::onLogEvent(const LogEvent& event) { return; } - const int32_t resetState = stateValue.mAnnotations.getResetState(); + const int32_t resetState = event.getResetState(); if (resetState != -1) { VLOG("StateTracker new reset state: %d", resetState); handleReset(eventTimeNs, resetState); diff --git a/cmds/statsd/src/stats_log.proto b/cmds/statsd/src/stats_log.proto index f4247eca6e23..868247bc9d64 100644 --- a/cmds/statsd/src/stats_log.proto +++ b/cmds/statsd/src/stats_log.proto @@ -425,6 +425,7 @@ message StatsdStatsReport { message AtomStats { optional int32 tag = 1; optional int32 count = 2; + optional int32 error_count = 3; } repeated AtomStats atom_stats = 7; @@ -460,6 +461,7 @@ message StatsdStatsReport { optional int64 empty_data = 15; optional int64 registered_count = 16; optional int64 unregistered_count = 17; + optional int32 atom_error_count = 18; } repeated PulledAtomStats pulled_atom_stats = 10; diff --git a/cmds/statsd/src/stats_log_util.cpp b/cmds/statsd/src/stats_log_util.cpp index 563531351fe0..f9fddc8a7c8a 100644 --- a/cmds/statsd/src/stats_log_util.cpp +++ b/cmds/statsd/src/stats_log_util.cpp @@ -80,6 +80,8 @@ const int FIELD_ID_STATS_COMPANION_BINDER_TRANSACTION_FAILED = 14; const int FIELD_ID_EMPTY_DATA = 15; const int FIELD_ID_PULL_REGISTERED_COUNT = 16; const int FIELD_ID_PULL_UNREGISTERED_COUNT = 17; +const int FIELD_ID_ATOM_ERROR_COUNT = 18; + // for AtomMetricStats proto const int FIELD_ID_ATOM_METRIC_STATS = 17; const int FIELD_ID_METRIC_ID = 1; @@ -492,6 +494,7 @@ void writePullerStatsToStream(const std::pair<int, StatsdStats::PulledAtomStats> (long long) pair.second.registeredCount); protoOutput->write(FIELD_TYPE_INT64 | FIELD_ID_PULL_UNREGISTERED_COUNT, (long long) pair.second.unregisteredCount); + protoOutput->write(FIELD_TYPE_INT32 | FIELD_ID_ATOM_ERROR_COUNT, pair.second.atomErrorCount); protoOutput->end(token); } diff --git a/cmds/statsd/src/utils/NamedLatch.cpp b/cmds/statsd/src/utils/MultiConditionTrigger.cpp index 6e77977857cc..43a69337f368 100644 --- a/cmds/statsd/src/utils/NamedLatch.cpp +++ b/cmds/statsd/src/utils/MultiConditionTrigger.cpp @@ -15,7 +15,9 @@ */ #define DEBUG false // STOPSHIP if true -#include "NamedLatch.h" +#include "MultiConditionTrigger.h" + +#include <thread> using namespace std; @@ -23,26 +25,33 @@ namespace android { namespace os { namespace statsd { -NamedLatch::NamedLatch(const set<string>& eventNames) : mRemainingEventNames(eventNames) { +MultiConditionTrigger::MultiConditionTrigger(const set<string>& conditionNames, + function<void()> trigger) + : mRemainingConditionNames(conditionNames), + mTrigger(trigger), + mCompleted(mRemainingConditionNames.empty()) { + if (mCompleted) { + thread executorThread([this] { mTrigger(); }); + executorThread.detach(); + } } -void NamedLatch::countDown(const string& eventName) { - bool notify = false; +void MultiConditionTrigger::markComplete(const string& conditionName) { + bool doTrigger = false; { lock_guard<mutex> lg(mMutex); - mRemainingEventNames.erase(eventName); - notify = mRemainingEventNames.empty(); + if (mCompleted) { + return; + } + mRemainingConditionNames.erase(conditionName); + mCompleted = mRemainingConditionNames.empty(); + doTrigger = mCompleted; } - if (notify) { - mConditionVariable.notify_all(); + if (doTrigger) { + std::thread executorThread([this] { mTrigger(); }); + executorThread.detach(); } } - -void NamedLatch::wait() const { - unique_lock<mutex> unique_lk(mMutex); - mConditionVariable.wait(unique_lk, [this] { return mRemainingEventNames.empty(); }); -} - } // namespace statsd } // namespace os } // namespace android diff --git a/cmds/statsd/src/utils/MultiConditionTrigger.h b/cmds/statsd/src/utils/MultiConditionTrigger.h new file mode 100644 index 000000000000..51f6029915be --- /dev/null +++ b/cmds/statsd/src/utils/MultiConditionTrigger.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2020 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. + */ +#pragma once + +#include <gtest/gtest_prod.h> + +#include <mutex> +#include <set> + +namespace android { +namespace os { +namespace statsd { + +/** + * This class provides a utility to wait for a set of named conditions to occur. + * + * It will execute the trigger runnable in a detached thread once all conditions have been marked + * true. + */ +class MultiConditionTrigger { +public: + explicit MultiConditionTrigger(const std::set<std::string>& conditionNames, + std::function<void()> trigger); + + MultiConditionTrigger(const MultiConditionTrigger&) = delete; + MultiConditionTrigger& operator=(const MultiConditionTrigger&) = delete; + + // Mark a specific condition as true. If this condition has called markComplete already or if + // the event was not specified in the constructor, the function is a no-op. + void markComplete(const std::string& eventName); + +private: + mutable std::mutex mMutex; + std::set<std::string> mRemainingConditionNames; + std::function<void()> mTrigger; + bool mCompleted; + + FRIEND_TEST(MultiConditionTriggerTest, TestCountDownCalledBySameEventName); +}; +} // namespace statsd +} // namespace os +} // namespace android diff --git a/cmds/statsd/src/utils/NamedLatch.h b/cmds/statsd/src/utils/NamedLatch.h deleted file mode 100644 index 70238370f647..000000000000 --- a/cmds/statsd/src/utils/NamedLatch.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2020 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. - */ -#pragma once - -#include <gtest/gtest_prod.h> - -#include <condition_variable> -#include <mutex> -#include <set> - -namespace android { -namespace os { -namespace statsd { - -/** - * This class provides a threading primitive similar to a latch. - * The primary difference is that it waits for named events to occur instead of waiting for - * N threads to reach a certain point. - * - * It uses a condition variable under the hood. - */ -class NamedLatch { -public: - explicit NamedLatch(const std::set<std::string>& eventNames); - - NamedLatch(const NamedLatch&) = delete; - NamedLatch& operator=(const NamedLatch&) = delete; - - // Mark a specific event as completed. If this event has called countDown already or if the - // event was not specified in the constructor, the function is a no-op. - void countDown(const std::string& eventName); - - // Blocks the calling thread until all events in eventNames have called countDown. - void wait() const; - -private: - mutable std::mutex mMutex; - mutable std::condition_variable mConditionVariable; - std::set<std::string> mRemainingEventNames; - - FRIEND_TEST(NamedLatchTest, TestCountDownCalledBySameEventName); -}; -} // namespace statsd -} // namespace os -} // namespace android diff --git a/cmds/statsd/tests/LogEvent_test.cpp b/cmds/statsd/tests/LogEvent_test.cpp index bb4578d9b701..e52e2d024e94 100644 --- a/cmds/statsd/tests/LogEvent_test.cpp +++ b/cmds/statsd/tests/LogEvent_test.cpp @@ -352,7 +352,7 @@ TEST(LogEventTest, TestResetStateAnnotation) { const vector<FieldValue>& values = event.getValues(); EXPECT_EQ(values.size(), 1); - EXPECT_EQ(values[0].mAnnotations.getResetState(), resetState); + EXPECT_EQ(event.getResetState(), resetState); } } // namespace statsd diff --git a/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp b/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp index b173ee0334a7..911762339b70 100644 --- a/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp +++ b/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp @@ -89,6 +89,7 @@ StatsdConfig MakeValueMetricConfig(int64_t minTime) { valueMetric->set_bucket(FIVE_MINUTES); valueMetric->set_min_bucket_size_nanos(minTime); valueMetric->set_use_absolute_value_on_reset(true); + valueMetric->set_skip_zero_diff_output(false); return config; } @@ -217,6 +218,35 @@ TEST(PartialBucketE2eTest, TestCountMetricSplitOnRemoval) { EXPECT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info(0).count()); } +TEST(PartialBucketE2eTest, TestCountMetricSplitOnBoot) { + shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr); + SendConfig(service, MakeConfig()); + int64_t start = getElapsedRealtimeNs(); // This is the start-time the metrics producers are + // initialized with. + + // Goes into the first bucket + service->mProcessor->OnLogEvent(CreateAppCrashEvent(start + NS_PER_SEC, 100).get()); + int64_t bootCompleteTimeNs = start + 2 * NS_PER_SEC; + service->mProcessor->onStatsdInitCompleted(bootCompleteTimeNs); + // Goes into the second bucket. + service->mProcessor->OnLogEvent(CreateAppCrashEvent(start + 3 * NS_PER_SEC, 100).get()); + + ConfigMetricsReport report = GetReports(service->mProcessor, start + 4 * NS_PER_SEC); + backfillStartEndTimestamp(&report); + + ASSERT_EQ(1, report.metrics_size()); + ASSERT_EQ(1, report.metrics(0).count_metrics().data_size()); + ASSERT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info_size()); + EXPECT_TRUE(report.metrics(0) + .count_metrics() + .data(0) + .bucket_info(0) + .has_start_bucket_elapsed_nanos()); + EXPECT_EQ(MillisToNano(NanoToMillis(bootCompleteTimeNs)), + report.metrics(0).count_metrics().data(0).bucket_info(0).end_bucket_elapsed_nanos()); + EXPECT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info(0).count()); +} + TEST(PartialBucketE2eTest, TestValueMetricWithoutMinPartialBucket) { shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr); service->mPullerManager->RegisterPullAtomCallback( @@ -229,13 +259,22 @@ TEST(PartialBucketE2eTest, TestValueMetricWithoutMinPartialBucket) { // initialized with. service->mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start); - service->mUidMap->updateApp(5 * 60 * NS_PER_SEC + start + 2, String16(kApp1.c_str()), 1, 2, - String16("v2"), String16("")); + int64_t appUpgradeTimeNs = 5 * 60 * NS_PER_SEC + start + 2 * NS_PER_SEC; + service->mUidMap->updateApp(appUpgradeTimeNs, String16(kApp1.c_str()), 1, 2, String16("v2"), + String16("")); ConfigMetricsReport report = - GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100, true); + GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC); + backfillStartEndTimestamp(&report); + EXPECT_EQ(1, report.metrics_size()); EXPECT_EQ(0, report.metrics(0).value_metrics().skipped_size()); + + // The fake subsystem state sleep puller returns two atoms. + ASSERT_EQ(2, report.metrics(0).value_metrics().data_size()); + ASSERT_EQ(2, report.metrics(0).value_metrics().data(0).bucket_info_size()); + EXPECT_EQ(MillisToNano(NanoToMillis(appUpgradeTimeNs)), + report.metrics(0).value_metrics().data(0).bucket_info(1).end_bucket_elapsed_nanos()); } TEST(PartialBucketE2eTest, TestValueMetricWithMinPartialBucket) { @@ -249,13 +288,13 @@ TEST(PartialBucketE2eTest, TestValueMetricWithMinPartialBucket) { int64_t start = getElapsedRealtimeNs(); // This is the start-time the metrics producers are // initialized with. - const int64_t endSkipped = 5 * 60 * NS_PER_SEC + start + 2; + const int64_t endSkipped = 5 * 60 * NS_PER_SEC + start + 2 * NS_PER_SEC; service->mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start); service->mUidMap->updateApp(endSkipped, String16(kApp1.c_str()), 1, 2, String16("v2"), String16("")); ConfigMetricsReport report = - GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true); + GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC); backfillStartEndTimestamp(&report); ASSERT_EQ(1, report.metrics_size()); @@ -264,10 +303,49 @@ TEST(PartialBucketE2eTest, TestValueMetricWithMinPartialBucket) { // Can't test the start time since it will be based on the actual time when the pulling occurs. EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)), report.metrics(0).value_metrics().skipped(0).end_bucket_elapsed_nanos()); + + ASSERT_EQ(2, report.metrics(0).value_metrics().data_size()); + EXPECT_EQ(1, report.metrics(0).value_metrics().data(0).bucket_info_size()); +} + +TEST(PartialBucketE2eTest, TestValueMetricOnBootWithoutMinPartialBucket) { + shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr); + // Initial pull will fail since puller is not registered. + SendConfig(service, MakeValueMetricConfig(0)); + int64_t start = getElapsedRealtimeNs(); // This is the start-time the metrics producers are + // initialized with. + + service->mPullerManager->RegisterPullAtomCallback( + /*uid=*/0, util::SUBSYSTEM_SLEEP_STATE, NS_PER_SEC, NS_PER_SEC * 10, {}, + SharedRefBase::make<FakeSubsystemSleepCallback>()); + + int64_t bootCompleteTimeNs = start + NS_PER_SEC; + service->mProcessor->onStatsdInitCompleted(bootCompleteTimeNs); + + service->mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start); + + ConfigMetricsReport report = GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100); + backfillStartEndTimestamp(&report); + + // First bucket is dropped due to the initial pull failing + ASSERT_EQ(1, report.metrics_size()); + EXPECT_EQ(1, report.metrics(0).value_metrics().skipped_size()); + EXPECT_EQ(MillisToNano(NanoToMillis(bootCompleteTimeNs)), + report.metrics(0).value_metrics().skipped(0).end_bucket_elapsed_nanos()); + + // The fake subsystem state sleep puller returns two atoms. + ASSERT_EQ(2, report.metrics(0).value_metrics().data_size()); + ASSERT_EQ(1, report.metrics(0).value_metrics().data(0).bucket_info_size()); + EXPECT_EQ( + MillisToNano(NanoToMillis(bootCompleteTimeNs)), + report.metrics(0).value_metrics().data(0).bucket_info(0).start_bucket_elapsed_nanos()); } TEST(PartialBucketE2eTest, TestGaugeMetricWithoutMinPartialBucket) { shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr); + service->mPullerManager->RegisterPullAtomCallback( + /*uid=*/0, util::SUBSYSTEM_SLEEP_STATE, NS_PER_SEC, NS_PER_SEC * 10, {}, + SharedRefBase::make<FakeSubsystemSleepCallback>()); // Partial buckets don't occur when app is first installed. service->mUidMap->updateApp(1, String16(kApp1.c_str()), 1, 1, String16("v1"), String16("")); SendConfig(service, MakeGaugeMetricConfig(0)); @@ -278,16 +356,22 @@ TEST(PartialBucketE2eTest, TestGaugeMetricWithoutMinPartialBucket) { service->mUidMap->updateApp(5 * 60 * NS_PER_SEC + start + 2, String16(kApp1.c_str()), 1, 2, String16("v2"), String16("")); - ConfigMetricsReport report = - GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100, true); - EXPECT_EQ(1, report.metrics_size()); + ConfigMetricsReport report = GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100); + backfillStartEndTimestamp(&report); + ASSERT_EQ(1, report.metrics_size()); EXPECT_EQ(0, report.metrics(0).gauge_metrics().skipped_size()); + // The fake subsystem state sleep puller returns two atoms. + ASSERT_EQ(2, report.metrics(0).gauge_metrics().data_size()); + EXPECT_EQ(2, report.metrics(0).gauge_metrics().data(0).bucket_info_size()); } TEST(PartialBucketE2eTest, TestGaugeMetricWithMinPartialBucket) { shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr); // Partial buckets don't occur when app is first installed. service->mUidMap->updateApp(1, String16(kApp1.c_str()), 1, 1, String16("v1"), String16("")); + service->mPullerManager->RegisterPullAtomCallback( + /*uid=*/0, util::SUBSYSTEM_SLEEP_STATE, NS_PER_SEC, NS_PER_SEC * 10, {}, + SharedRefBase::make<FakeSubsystemSleepCallback>()); SendConfig(service, MakeGaugeMetricConfig(60 * NS_PER_SEC /* One minute */)); int64_t start = getElapsedRealtimeNs(); // This is the start-time the metrics producers are // initialized with. @@ -298,7 +382,7 @@ TEST(PartialBucketE2eTest, TestGaugeMetricWithMinPartialBucket) { String16("")); ConfigMetricsReport report = - GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true); + GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC); backfillStartEndTimestamp(&report); ASSERT_EQ(1, report.metrics_size()); ASSERT_EQ(1, report.metrics(0).gauge_metrics().skipped_size()); @@ -306,6 +390,38 @@ TEST(PartialBucketE2eTest, TestGaugeMetricWithMinPartialBucket) { EXPECT_TRUE(report.metrics(0).gauge_metrics().skipped(0).has_start_bucket_elapsed_nanos()); EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)), report.metrics(0).gauge_metrics().skipped(0).end_bucket_elapsed_nanos()); + ASSERT_EQ(2, report.metrics(0).gauge_metrics().data_size()); + EXPECT_EQ(1, report.metrics(0).gauge_metrics().data(0).bucket_info_size()); +} + +TEST(PartialBucketE2eTest, TestGaugeMetricOnBootWithoutMinPartialBucket) { + shared_ptr<StatsService> service = SharedRefBase::make<StatsService>(nullptr, nullptr); + // Initial pull will fail since puller hasn't been registered. + SendConfig(service, MakeGaugeMetricConfig(0)); + int64_t start = getElapsedRealtimeNs(); // This is the start-time the metrics producers are + // initialized with. + + service->mPullerManager->RegisterPullAtomCallback( + /*uid=*/0, util::SUBSYSTEM_SLEEP_STATE, NS_PER_SEC, NS_PER_SEC * 10, {}, + SharedRefBase::make<FakeSubsystemSleepCallback>()); + + int64_t bootCompleteTimeNs = start + NS_PER_SEC; + service->mProcessor->onStatsdInitCompleted(bootCompleteTimeNs); + + service->mProcessor->informPullAlarmFired(5 * 60 * NS_PER_SEC + start); + + ConfigMetricsReport report = GetReports(service->mProcessor, 5 * 60 * NS_PER_SEC + start + 100); + backfillStartEndTimestamp(&report); + + ASSERT_EQ(1, report.metrics_size()); + EXPECT_EQ(0, report.metrics(0).gauge_metrics().skipped_size()); + // The fake subsystem state sleep puller returns two atoms. + ASSERT_EQ(2, report.metrics(0).gauge_metrics().data_size()); + // No data in the first bucket, so nothing is reported + ASSERT_EQ(1, report.metrics(0).gauge_metrics().data(0).bucket_info_size()); + EXPECT_EQ( + MillisToNano(NanoToMillis(bootCompleteTimeNs)), + report.metrics(0).gauge_metrics().data(0).bucket_info(0).start_bucket_elapsed_nanos()); } #else diff --git a/cmds/statsd/tests/guardrail/StatsdStats_test.cpp b/cmds/statsd/tests/guardrail/StatsdStats_test.cpp index 129fafa75b41..cdde603f4c0b 100644 --- a/cmds/statsd/tests/guardrail/StatsdStats_test.cpp +++ b/cmds/statsd/tests/guardrail/StatsdStats_test.cpp @@ -486,6 +486,41 @@ TEST(StatsdStatsTest, TestActivationBroadcastGuardrailHit) { EXPECT_TRUE(uid2Good); } +TEST(StatsdStatsTest, TestAtomErrorStats) { + StatsdStats stats; + + int pushAtomTag = 100; + int pullAtomTag = 1000; + int numErrors = 10; + + for (int i = 0; i < numErrors; i++) { + // We must call noteAtomLogged as well because only those pushed atoms + // that have been logged will have stats printed about them in the + // proto. + stats.noteAtomLogged(pushAtomTag, /*timeSec=*/0); + stats.noteAtomError(pushAtomTag, /*pull=*/false); + + stats.noteAtomError(pullAtomTag, /*pull=*/true); + } + + vector<uint8_t> output; + stats.dumpStats(&output, false); + StatsdStatsReport report; + EXPECT_TRUE(report.ParseFromArray(&output[0], output.size())); + + // Check error count = numErrors for push atom + EXPECT_EQ(1, report.atom_stats_size()); + const auto& pushedAtomStats = report.atom_stats(0); + EXPECT_EQ(pushAtomTag, pushedAtomStats.tag()); + EXPECT_EQ(numErrors, pushedAtomStats.error_count()); + + // Check error count = numErrors for pull atom + EXPECT_EQ(1, report.pulled_atom_stats_size()); + const auto& pulledAtomStats = report.pulled_atom_stats(0); + EXPECT_EQ(pullAtomTag, pulledAtomStats.atom_id()); + EXPECT_EQ(numErrors, pulledAtomStats.atom_error_count()); +} + } // namespace statsd } // namespace os } // namespace android diff --git a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp index 65f8de69711d..8131725cd148 100644 --- a/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp +++ b/cmds/statsd/tests/metrics/CountMetricProducer_test.cpp @@ -38,9 +38,9 @@ namespace android { namespace os { namespace statsd { -const ConfigKey kConfigKey(0, 12345); namespace { +const ConfigKey kConfigKey(0, 12345); void makeLogEvent(LogEvent* logEvent, int64_t timestampNs, int atomId) { AStatsEvent* statsEvent = AStatsEvent_obtain(); @@ -61,6 +61,13 @@ void makeLogEvent(LogEvent* logEvent, int64_t timestampNs, int atomId, string ui } // namespace +// Setup for parameterized tests. +class CountMetricProducerTest_PartialBucket : public TestWithParam<BucketSplitEvent> {}; + +INSTANTIATE_TEST_SUITE_P(CountMetricProducerTest_PartialBucket, + CountMetricProducerTest_PartialBucket, + testing::Values(APP_UPGRADE, BOOT_COMPLETE)); + TEST(CountMetricProducerTest, TestFirstBucket) { CountMetric metric; metric.set_id(1); @@ -237,11 +244,11 @@ TEST(CountMetricProducerTest, TestEventsWithSlicedCondition) { EXPECT_EQ(1LL, bucketInfo.mCount); } -TEST(CountMetricProducerTest, TestEventWithAppUpgrade) { +TEST_P(CountMetricProducerTest_PartialBucket, TestSplitInCurrentBucket) { sp<AlarmMonitor> alarmMonitor; int64_t bucketStartTimeNs = 10000000000; int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL; - int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; + int64_t eventTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; int tagId = 1; int conditionTagId = 2; @@ -260,22 +267,30 @@ TEST(CountMetricProducerTest, TestEventWithAppUpgrade) { sp<AnomalyTracker> anomalyTracker = countProducer.addAnomalyTracker(alert, alarmMonitor); EXPECT_TRUE(anomalyTracker != nullptr); - // Bucket is flushed yet. + // Bucket is not flushed yet. LogEvent event1(/*uid=*/0, /*pid=*/0); makeLogEvent(&event1, bucketStartTimeNs + 1, tagId, /*uid=*/"111"); countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1); EXPECT_EQ(0UL, countProducer.mPastBuckets.size()); EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY)); - // App upgrade forces bucket flush. + // App upgrade or boot complete forces bucket flush. // Check that there's a past bucket and the bucket end is not adjusted. - countProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + switch (GetParam()) { + case APP_UPGRADE: + countProducer.notifyAppUpgrade(eventTimeNs); + break; + case BOOT_COMPLETE: + countProducer.onStatsdInitCompleted(eventTimeNs); + break; + } EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ((long long)bucketStartTimeNs, + EXPECT_EQ(bucketStartTimeNs, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs); - EXPECT_EQ((long long)eventUpgradeTimeNs, + EXPECT_EQ(eventTimeNs, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs); - EXPECT_EQ(eventUpgradeTimeNs, countProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(0, countProducer.getCurrentBucketNum()); + EXPECT_EQ(eventTimeNs, countProducer.mCurrentBucketStartTimeNs); // Anomaly tracker only contains full buckets. EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY)); @@ -285,7 +300,8 @@ TEST(CountMetricProducerTest, TestEventWithAppUpgrade) { makeLogEvent(&event2, bucketStartTimeNs + 59 * NS_PER_SEC + 10, tagId, /*uid=*/"222"); countProducer.onMatchedLogEvent(1 /*log matcher index*/, event2); EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ(eventUpgradeTimeNs, countProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(eventTimeNs, countProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(0, countProducer.getCurrentBucketNum()); EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY)); // Third event in following bucket. @@ -294,13 +310,14 @@ TEST(CountMetricProducerTest, TestEventWithAppUpgrade) { countProducer.onMatchedLogEvent(1 /*log matcher index*/, event3); EXPECT_EQ(2UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); EXPECT_EQ(lastEndTimeNs, countProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(1, countProducer.getCurrentBucketNum()); EXPECT_EQ(2, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY)); } -TEST(CountMetricProducerTest, TestEventWithAppUpgradeInNextBucket) { +TEST_P(CountMetricProducerTest_PartialBucket, TestSplitInNextBucket) { int64_t bucketStartTimeNs = 10000000000; int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL; - int64_t eventUpgradeTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC; + int64_t eventTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC; int tagId = 1; int conditionTagId = 2; @@ -319,15 +336,23 @@ TEST(CountMetricProducerTest, TestEventWithAppUpgradeInNextBucket) { countProducer.onMatchedLogEvent(1 /*log matcher index*/, event1); EXPECT_EQ(0UL, countProducer.mPastBuckets.size()); - // App upgrade forces bucket flush. - // Check that there's a past bucket and the bucket end is not adjusted. - countProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + // App upgrade or boot complete forces bucket flush. + // Check that there's a past bucket and the bucket end is not adjusted since the upgrade + // occurred after the bucket end time. + switch (GetParam()) { + case APP_UPGRADE: + countProducer.notifyAppUpgrade(eventTimeNs); + break; + case BOOT_COMPLETE: + countProducer.onStatsdInitCompleted(eventTimeNs); + break; + } EXPECT_EQ(1UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ((int64_t)bucketStartTimeNs, + EXPECT_EQ(bucketStartTimeNs, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs); EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs); - EXPECT_EQ(eventUpgradeTimeNs, countProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(eventTimeNs, countProducer.mCurrentBucketStartTimeNs); // Next event occurs in same bucket as partial bucket created. LogEvent event2(/*uid=*/0, /*pid=*/0); @@ -340,7 +365,7 @@ TEST(CountMetricProducerTest, TestEventWithAppUpgradeInNextBucket) { makeLogEvent(&event3, bucketStartTimeNs + 121 * NS_PER_SEC + 10, tagId, /*uid=*/"333"); countProducer.onMatchedLogEvent(1 /*log matcher index*/, event3); EXPECT_EQ(2UL, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ((int64_t)eventUpgradeTimeNs, + EXPECT_EQ((int64_t)eventTimeNs, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][1].mBucketStartNs); EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, countProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][1].mBucketEndNs); diff --git a/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp b/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp index 30f815962160..8ef251952db7 100644 --- a/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp +++ b/cmds/statsd/tests/metrics/DurationMetricProducer_test.cpp @@ -41,10 +41,10 @@ namespace android { namespace os { namespace statsd { -const ConfigKey kConfigKey(0, 12345); namespace { +const ConfigKey kConfigKey(0, 12345); void makeLogEvent(LogEvent* logEvent, int64_t timestampNs, int atomId) { AStatsEvent* statsEvent = AStatsEvent_obtain(); AStatsEvent_setAtomId(statsEvent, atomId); @@ -55,6 +55,13 @@ void makeLogEvent(LogEvent* logEvent, int64_t timestampNs, int atomId) { } // namespace +// Setup for parameterized tests. +class DurationMetricProducerTest_PartialBucket : public TestWithParam<BucketSplitEvent> {}; + +INSTANTIATE_TEST_SUITE_P(DurationMetricProducerTest_PartialBucket, + DurationMetricProducerTest_PartialBucket, + testing::Values(APP_UPGRADE, BOOT_COMPLETE)); + TEST(DurationMetricTrackerTest, TestFirstBucket) { sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); DurationMetric metric; @@ -205,7 +212,7 @@ TEST(DurationMetricTrackerTest, TestNonSlicedConditionUnknownState) { EXPECT_EQ(1LL, buckets2[0].mDuration); } -TEST(DurationMetricTrackerTest, TestSumDurationWithUpgrade) { +TEST_P(DurationMetricProducerTest_PartialBucket, TestSumDuration) { /** * The duration starts from the first bucket, through the two partial buckets (10-70sec), * another bucket, and ends at the beginning of the next full bucket. @@ -217,15 +224,7 @@ TEST(DurationMetricTrackerTest, TestSumDurationWithUpgrade) { */ int64_t bucketStartTimeNs = 10000000000; int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL; - int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; - int64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC; - int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC; - int tagId = 1; - LogEvent event1(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event1, startTimeNs, tagId); - LogEvent event2(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event2, endTimeNs, tagId); DurationMetric metric; metric.set_id(1); @@ -238,32 +237,47 @@ TEST(DurationMetricTrackerTest, TestSumDurationWithUpgrade) { 3 /* stop_all index */, false /*nesting*/, wizard, dimensions, bucketStartTimeNs, bucketStartTimeNs); + int64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC; + LogEvent event1(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event1, startTimeNs, tagId); durationProducer.onMatchedLogEvent(1 /* start index*/, event1); EXPECT_EQ(0UL, durationProducer.mPastBuckets.size()); EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs); - durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; + switch (GetParam()) { + case APP_UPGRADE: + durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } EXPECT_EQ(1UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); std::vector<DurationBucket> buckets = durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY]; EXPECT_EQ(bucketStartTimeNs, buckets[0].mBucketStartNs); - EXPECT_EQ(eventUpgradeTimeNs, buckets[0].mBucketEndNs); - EXPECT_EQ(eventUpgradeTimeNs - startTimeNs, buckets[0].mDuration); - EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(partialBucketSplitTimeNs, buckets[0].mBucketEndNs); + EXPECT_EQ(partialBucketSplitTimeNs - startTimeNs, buckets[0].mDuration); + EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(0, durationProducer.getCurrentBucketNum()); // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket. + int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC; + LogEvent event2(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event2, endTimeNs, tagId); durationProducer.onMatchedLogEvent(2 /* stop index*/, event2); buckets = durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY]; EXPECT_EQ(3UL, buckets.size()); - EXPECT_EQ(eventUpgradeTimeNs, buckets[1].mBucketStartNs); + EXPECT_EQ(partialBucketSplitTimeNs, buckets[1].mBucketStartNs); EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[1].mBucketEndNs); - EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - eventUpgradeTimeNs, buckets[1].mDuration); + EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - partialBucketSplitTimeNs, buckets[1].mDuration); EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[2].mBucketStartNs); EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[2].mBucketEndNs); EXPECT_EQ(bucketSizeNs, buckets[2].mDuration); } -TEST(DurationMetricTrackerTest, TestSumDurationWithUpgradeInFollowingBucket) { +TEST_P(DurationMetricProducerTest_PartialBucket, TestSumDurationWithSplitInFollowingBucket) { /** * Expected buckets (start at 11s, upgrade at 75s, end at 135s): * - [10,70]: 59 secs @@ -272,15 +286,7 @@ TEST(DurationMetricTrackerTest, TestSumDurationWithUpgradeInFollowingBucket) { */ int64_t bucketStartTimeNs = 10000000000; int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL; - int64_t eventUpgradeTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC; - int64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC; - int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC; - int tagId = 1; - LogEvent event1(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event1, startTimeNs, tagId); - LogEvent event2(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event2, endTimeNs, tagId); DurationMetric metric; metric.set_id(1); @@ -293,11 +299,22 @@ TEST(DurationMetricTrackerTest, TestSumDurationWithUpgradeInFollowingBucket) { 3 /* stop_all index */, false /*nesting*/, wizard, dimensions, bucketStartTimeNs, bucketStartTimeNs); + int64_t startTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC; + LogEvent event1(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event1, startTimeNs, tagId); durationProducer.onMatchedLogEvent(1 /* start index*/, event1); EXPECT_EQ(0UL, durationProducer.mPastBuckets.size()); EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs); - durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC; + switch (GetParam()) { + case APP_UPGRADE: + durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } EXPECT_EQ(2UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); std::vector<DurationBucket> buckets = durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY]; @@ -305,32 +322,29 @@ TEST(DurationMetricTrackerTest, TestSumDurationWithUpgradeInFollowingBucket) { EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[0].mBucketEndNs); EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - startTimeNs, buckets[0].mDuration); EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, buckets[1].mBucketStartNs); - EXPECT_EQ(eventUpgradeTimeNs, buckets[1].mBucketEndNs); - EXPECT_EQ(eventUpgradeTimeNs - (bucketStartTimeNs + bucketSizeNs), buckets[1].mDuration); - EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(partialBucketSplitTimeNs, buckets[1].mBucketEndNs); + EXPECT_EQ(partialBucketSplitTimeNs - (bucketStartTimeNs + bucketSizeNs), buckets[1].mDuration); + EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(1, durationProducer.getCurrentBucketNum()); // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket. + int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC; + LogEvent event2(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event2, endTimeNs, tagId); durationProducer.onMatchedLogEvent(2 /* stop index*/, event2); buckets = durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY]; EXPECT_EQ(3UL, buckets.size()); - EXPECT_EQ(eventUpgradeTimeNs, buckets[2].mBucketStartNs); + EXPECT_EQ(partialBucketSplitTimeNs, buckets[2].mBucketStartNs); EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[2].mBucketEndNs); - EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs - eventUpgradeTimeNs, buckets[2].mDuration); + EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs - partialBucketSplitTimeNs, + buckets[2].mDuration); } -TEST(DurationMetricTrackerTest, TestSumDurationAnomalyWithUpgrade) { +TEST_P(DurationMetricProducerTest_PartialBucket, TestSumDurationAnomaly) { sp<AlarmMonitor> alarmMonitor; int64_t bucketStartTimeNs = 10000000000; int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL; - int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; - int64_t startTimeNs = bucketStartTimeNs + 1; - int64_t endTimeNs = startTimeNs + 65 * NS_PER_SEC; - int tagId = 1; - LogEvent event1(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event1, startTimeNs, tagId); - LogEvent event2(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event2, endTimeNs, tagId); // Setup metric with alert. DurationMetric metric; @@ -351,27 +365,35 @@ TEST(DurationMetricTrackerTest, TestSumDurationAnomalyWithUpgrade) { sp<AnomalyTracker> anomalyTracker = durationProducer.addAnomalyTracker(alert, alarmMonitor); EXPECT_TRUE(anomalyTracker != nullptr); + int64_t startTimeNs = bucketStartTimeNs + 1; + LogEvent event1(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event1, startTimeNs, tagId); durationProducer.onMatchedLogEvent(1 /* start index*/, event1); - durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + + int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; + switch (GetParam()) { + case APP_UPGRADE: + durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket. + int64_t endTimeNs = startTimeNs + 65 * NS_PER_SEC; + LogEvent event2(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event2, endTimeNs, tagId); durationProducer.onMatchedLogEvent(2 /* stop index*/, event2); + EXPECT_EQ(bucketStartTimeNs + bucketSizeNs - startTimeNs, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY)); } -TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgrade) { +TEST_P(DurationMetricProducerTest_PartialBucket, TestMaxDuration) { int64_t bucketStartTimeNs = 10000000000; int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL; - int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; - int64_t startTimeNs = bucketStartTimeNs + 1; - int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC; - int tagId = 1; - LogEvent event1(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event1, startTimeNs, tagId); - LogEvent event2(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event2, endTimeNs, tagId); DurationMetric metric; metric.set_id(1); @@ -385,15 +407,30 @@ TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgrade) { 3 /* stop_all index */, false /*nesting*/, wizard, dimensions, bucketStartTimeNs, bucketStartTimeNs); + int64_t startTimeNs = bucketStartTimeNs + 1; + LogEvent event1(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event1, startTimeNs, tagId); durationProducer.onMatchedLogEvent(1 /* start index*/, event1); EXPECT_EQ(0UL, durationProducer.mPastBuckets.size()); EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs); - durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; + switch (GetParam()) { + case APP_UPGRADE: + durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(0, durationProducer.getCurrentBucketNum()); // We skip ahead one bucket, so we fill in the first two partial buckets and one full bucket. + int64_t endTimeNs = startTimeNs + 125 * NS_PER_SEC; + LogEvent event2(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event2, endTimeNs, tagId); durationProducer.onMatchedLogEvent(2 /* stop index*/, event2); EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); @@ -406,18 +443,10 @@ TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgrade) { EXPECT_EQ(endTimeNs - startTimeNs, buckets[0].mDuration); } -TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgradeInNextBucket) { +TEST_P(DurationMetricProducerTest_PartialBucket, TestMaxDurationWithSplitInNextBucket) { int64_t bucketStartTimeNs = 10000000000; int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000LL; - int64_t eventUpgradeTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC; - int64_t startTimeNs = bucketStartTimeNs + 1; - int64_t endTimeNs = startTimeNs + 115 * NS_PER_SEC; - int tagId = 1; - LogEvent event1(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event1, startTimeNs, tagId); - LogEvent event2(/*uid=*/0, /*pid=*/0); - makeLogEvent(&event2, endTimeNs, tagId); DurationMetric metric; metric.set_id(1); @@ -431,24 +460,39 @@ TEST(DurationMetricTrackerTest, TestMaxDurationWithUpgradeInNextBucket) { 3 /* stop_all index */, false /*nesting*/, wizard, dimensions, bucketStartTimeNs, bucketStartTimeNs); + int64_t startTimeNs = bucketStartTimeNs + 1; + LogEvent event1(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event1, startTimeNs, tagId); durationProducer.onMatchedLogEvent(1 /* start index*/, event1); EXPECT_EQ(0UL, durationProducer.mPastBuckets.size()); EXPECT_EQ(bucketStartTimeNs, durationProducer.mCurrentBucketStartTimeNs); - durationProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 65 * NS_PER_SEC; + switch (GetParam()) { + case APP_UPGRADE: + durationProducer.notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + durationProducer.onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(1, durationProducer.getCurrentBucketNum()); // Stop occurs in the same partial bucket as created for the app upgrade. + int64_t endTimeNs = startTimeNs + 115 * NS_PER_SEC; + LogEvent event2(/*uid=*/0, /*pid=*/0); + makeLogEvent(&event2, endTimeNs, tagId); durationProducer.onMatchedLogEvent(2 /* stop index*/, event2); EXPECT_EQ(0UL, durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ(eventUpgradeTimeNs, durationProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(partialBucketSplitTimeNs, durationProducer.mCurrentBucketStartTimeNs); durationProducer.flushIfNeededLocked(bucketStartTimeNs + 2 * bucketSizeNs + 1); std::vector<DurationBucket> buckets = durationProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY]; EXPECT_EQ(1UL, buckets.size()); - EXPECT_EQ(eventUpgradeTimeNs, buckets[0].mBucketStartNs); + EXPECT_EQ(partialBucketSplitTimeNs, buckets[0].mBucketStartNs); EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs, buckets[0].mBucketEndNs); EXPECT_EQ(endTimeNs - startTimeNs, buckets[0].mDuration); } diff --git a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp index 42d0d5d8c530..9d2ec88e2f9b 100644 --- a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp +++ b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp @@ -42,6 +42,8 @@ namespace android { namespace os { namespace statsd { +namespace { + const ConfigKey kConfigKey(0, 12345); const int tagId = 1; const int64_t metricId = 123; @@ -52,9 +54,8 @@ const int64_t bucketSizeNs = TimeUnitToBucketSizeInMillis(ONE_MINUTE) * 1000000L const int64_t bucket2StartTimeNs = bucketStartTimeNs + bucketSizeNs; const int64_t bucket3StartTimeNs = bucketStartTimeNs + 2 * bucketSizeNs; const int64_t bucket4StartTimeNs = bucketStartTimeNs + 3 * bucketSizeNs; -const int64_t eventUpgradeTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; +const int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 15 * NS_PER_SEC; -namespace { shared_ptr<LogEvent> makeLogEvent(int32_t atomId, int64_t timestampNs, int32_t value1, string str1, int32_t value2) { AStatsEvent* statsEvent = AStatsEvent_obtain(); @@ -71,6 +72,13 @@ shared_ptr<LogEvent> makeLogEvent(int32_t atomId, int64_t timestampNs, int32_t v } } // anonymous namespace +// Setup for parameterized tests. +class GaugeMetricProducerTest_PartialBucket : public TestWithParam<BucketSplitEvent> {}; + +INSTANTIATE_TEST_SUITE_P(GaugeMetricProducerTest_PartialBucket, + GaugeMetricProducerTest_PartialBucket, + testing::Values(APP_UPGRADE, BOOT_COMPLETE)); + /* * Tests that the first bucket works correctly */ @@ -194,7 +202,7 @@ TEST(GaugeMetricProducerTest, TestPulledEventsNoCondition) { EXPECT_EQ(25L, it->mValue.int_value); } -TEST(GaugeMetricProducerTest, TestPushedEventsWithUpgrade) { +TEST_P(GaugeMetricProducerTest_PartialBucket, TestPushedEvents) { sp<AlarmMonitor> alarmMonitor; GaugeMetric metric; metric.set_id(metricId); @@ -230,11 +238,22 @@ TEST(GaugeMetricProducerTest, TestPushedEventsWithUpgrade) { gaugeProducer.onMatchedLogEvent(1 /*log matcher index*/, event1); EXPECT_EQ(1UL, (*gaugeProducer.mCurrentSlicedBucket).count(DEFAULT_METRIC_DIMENSION_KEY)); - gaugeProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + switch (GetParam()) { + case APP_UPGRADE: + gaugeProducer.notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + gaugeProducer.onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } EXPECT_EQ(0UL, (*gaugeProducer.mCurrentSlicedBucket).count(DEFAULT_METRIC_DIMENSION_KEY)); EXPECT_EQ(1UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); + EXPECT_EQ(bucketStartTimeNs, + gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs); + EXPECT_EQ(partialBucketSplitTimeNs, + gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs); EXPECT_EQ(0L, gaugeProducer.mCurrentBucketNum); - EXPECT_EQ(eventUpgradeTimeNs, gaugeProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(partialBucketSplitTimeNs, gaugeProducer.mCurrentBucketStartTimeNs); // Partial buckets are not sent to anomaly tracker. EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY)); @@ -244,7 +263,11 @@ TEST(GaugeMetricProducerTest, TestPushedEventsWithUpgrade) { gaugeProducer.onMatchedLogEvent(1 /*log matcher index*/, event2); EXPECT_EQ(0L, gaugeProducer.mCurrentBucketNum); EXPECT_EQ(1UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ((int64_t)eventUpgradeTimeNs, gaugeProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(bucketStartTimeNs, + gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs); + EXPECT_EQ(partialBucketSplitTimeNs, + gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs); + EXPECT_EQ((int64_t)partialBucketSplitTimeNs, gaugeProducer.mCurrentBucketStartTimeNs); // Partial buckets are not sent to anomaly tracker. EXPECT_EQ(0, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY)); @@ -267,7 +290,7 @@ TEST(GaugeMetricProducerTest, TestPushedEventsWithUpgrade) { EXPECT_EQ(2, anomalyTracker->getSumOverPastBuckets(DEFAULT_METRIC_DIMENSION_KEY)); } -TEST(GaugeMetricProducerTest, TestPulledWithUpgrade) { +TEST_P(GaugeMetricProducerTest_PartialBucket, TestPulled) { GaugeMetric metric; metric.set_id(metricId); metric.set_bucket(ONE_MINUTE); @@ -293,7 +316,8 @@ TEST(GaugeMetricProducerTest, TestPulledWithUpgrade) { .WillOnce(Invoke( [](int tagId, const ConfigKey&, vector<std::shared_ptr<LogEvent>>* data, bool) { data->clear(); - data->push_back(CreateRepeatedValueLogEvent(tagId, eventUpgradeTimeNs, 2)); + data->push_back( + CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs, 2)); return true; })); @@ -311,10 +335,21 @@ TEST(GaugeMetricProducerTest, TestPulledWithUpgrade) { .mFields->begin() ->mValue.int_value); - gaugeProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + switch (GetParam()) { + case APP_UPGRADE: + gaugeProducer.notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + gaugeProducer.onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } EXPECT_EQ(1UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); + EXPECT_EQ(bucketStartTimeNs, + gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs); + EXPECT_EQ(partialBucketSplitTimeNs, + gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketEndNs); EXPECT_EQ(0L, gaugeProducer.mCurrentBucketNum); - EXPECT_EQ((int64_t)eventUpgradeTimeNs, gaugeProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(partialBucketSplitTimeNs, gaugeProducer.mCurrentBucketStartTimeNs); EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size()); EXPECT_EQ(2, gaugeProducer.mCurrentSlicedBucket->begin() ->second.front() @@ -370,7 +405,7 @@ TEST(GaugeMetricProducerTest, TestPulledWithAppUpgradeDisabled) { .mFields->begin() ->mValue.int_value); - gaugeProducer.notifyAppUpgrade(eventUpgradeTimeNs, "ANY.APP", 1, 1); + gaugeProducer.notifyAppUpgrade(partialBucketSplitTimeNs); EXPECT_EQ(0UL, gaugeProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); EXPECT_EQ(0L, gaugeProducer.mCurrentBucketNum); EXPECT_EQ(bucketStartTimeNs, gaugeProducer.mCurrentBucketStartTimeNs); diff --git a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp index 3b4d646f0f2f..f493cc4033ad 100644 --- a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp +++ b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp @@ -41,6 +41,8 @@ namespace android { namespace os { namespace statsd { +namespace { + const ConfigKey kConfigKey(0, 12345); const int tagId = 1; const int64_t metricId = 123; @@ -58,10 +60,18 @@ double epsilon = 0.001; static void assertPastBucketValuesSingleKey( const std::unordered_map<MetricDimensionKey, std::vector<ValueBucket>>& mPastBuckets, const std::initializer_list<int>& expectedValuesList, - const std::initializer_list<int64_t>& expectedDurationNsList) { - std::vector<int> expectedValues(expectedValuesList); - std::vector<int64_t> expectedDurationNs(expectedDurationNsList); + const std::initializer_list<int64_t>& expectedDurationNsList, + const std::initializer_list<int64_t>& expectedStartTimeNsList, + const std::initializer_list<int64_t>& expectedEndTimeNsList) { + vector<int> expectedValues(expectedValuesList); + vector<int64_t> expectedDurationNs(expectedDurationNsList); + vector<int64_t> expectedStartTimeNs(expectedStartTimeNsList); + vector<int64_t> expectedEndTimeNs(expectedEndTimeNsList); + ASSERT_EQ(expectedValues.size(), expectedDurationNs.size()); + ASSERT_EQ(expectedValues.size(), expectedStartTimeNs.size()); + ASSERT_EQ(expectedValues.size(), expectedEndTimeNs.size()); + if (expectedValues.size() == 0) { ASSERT_EQ(0, mPastBuckets.size()); return; @@ -70,15 +80,21 @@ static void assertPastBucketValuesSingleKey( ASSERT_EQ(1, mPastBuckets.size()); ASSERT_EQ(expectedValues.size(), mPastBuckets.begin()->second.size()); - auto buckets = mPastBuckets.begin()->second; + const vector<ValueBucket>& buckets = mPastBuckets.begin()->second; for (int i = 0; i < expectedValues.size(); i++) { EXPECT_EQ(expectedValues[i], buckets[i].values[0].long_value) << "Values differ at index " << i; EXPECT_EQ(expectedDurationNs[i], buckets[i].mConditionTrueNs) << "Condition duration value differ at index " << i; + EXPECT_EQ(expectedStartTimeNs[i], buckets[i].mBucketStartNs) + << "Start time differs at index " << i; + EXPECT_EQ(expectedEndTimeNs[i], buckets[i].mBucketEndNs) + << "End time differs at index " << i; } } +} // anonymous namespace + class ValueMetricProducerTestHelper { public: static sp<ValueMetricProducer> createValueProducerNoConditions( @@ -191,6 +207,13 @@ public: } }; +// Setup for parameterized tests. +class ValueMetricProducerTest_PartialBucket : public TestWithParam<BucketSplitEvent> {}; + +INSTANTIATE_TEST_SUITE_P(ValueMetricProducerTest_PartialBucket, + ValueMetricProducerTest_PartialBucket, + testing::Values(APP_UPGRADE, BOOT_COMPLETE)); + /* * Tests that the first bucket works correctly */ @@ -325,9 +348,10 @@ TEST(ValueMetricProducerTest, TestPulledEventsNoCondition) { EXPECT_EQ(bucketSizeNs, valueProducer->mPastBuckets.begin()->second[2].mConditionTrueNs); } -TEST(ValueMetricProducerTest, TestPartialBucketCreated) { +TEST_P(ValueMetricProducerTest_PartialBucket, TestPartialBucketCreated) { ValueMetric metric = ValueMetricProducerTestHelper::createMetric(); sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>(); + int64_t partialBucketSplitTimeNs = bucket2StartTimeNs + 2; EXPECT_CALL(*pullerManager, Pull(tagId, kConfigKey, _, _)) // Initialize bucket. .WillOnce(Invoke([](int tagId, const ConfigKey&, @@ -337,10 +361,12 @@ TEST(ValueMetricProducerTest, TestPartialBucketCreated) { return true; })) // Partial bucket. - .WillOnce(Invoke([](int tagId, const ConfigKey&, - vector<std::shared_ptr<LogEvent>>* data, bool) { + .WillOnce(Invoke([partialBucketSplitTimeNs](int tagId, const ConfigKey&, + vector<std::shared_ptr<LogEvent>>* data, + bool) { data->clear(); - data->push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 10, 5)); + data->push_back( + CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs + 8, 5)); return true; })); @@ -354,19 +380,21 @@ TEST(ValueMetricProducerTest, TestPartialBucketCreated) { valueProducer->onDataPulled(allData, /** success */ true, bucket2StartTimeNs); // Partial buckets created in 2nd bucket. - valueProducer->notifyAppUpgrade(bucket2StartTimeNs + 2, "com.foo", 10000, 1); + switch (GetParam()) { + case APP_UPGRADE: + valueProducer->notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + valueProducer->onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } + EXPECT_EQ(partialBucketSplitTimeNs, valueProducer->mCurrentBucketStartTimeNs); + EXPECT_EQ(1, valueProducer->getCurrentBucketNum()); - // One full bucket and one partial bucket. - EXPECT_EQ(1UL, valueProducer->mPastBuckets.size()); - vector<ValueBucket> buckets = valueProducer->mPastBuckets.begin()->second; - EXPECT_EQ(2UL, buckets.size()); - // Full bucket (2 - 1) - EXPECT_EQ(1, buckets[0].values[0].long_value); - EXPECT_EQ(bucketSizeNs, buckets[0].mConditionTrueNs); - // Full bucket (5 - 3) - EXPECT_EQ(3, buckets[1].values[0].long_value); - // partial bucket [bucket2StartTimeNs, bucket2StartTimeNs + 2] - EXPECT_EQ(2, buckets[1].mConditionTrueNs); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {1, 3}, + {bucketSizeNs, partialBucketSplitTimeNs - bucket2StartTimeNs}, + {bucketStartTimeNs, bucket2StartTimeNs}, + {bucket2StartTimeNs, partialBucketSplitTimeNs}); } /* @@ -613,7 +641,8 @@ TEST(ValueMetricProducerTest, TestEventsWithNonSlicedCondition) { allData.clear(); allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 1, 110)); valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs - 8}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs - 8}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); // has one slice EXPECT_EQ(1UL, valueProducer->mCurrentSlicedBucket.size()); @@ -625,7 +654,8 @@ TEST(ValueMetricProducerTest, TestEventsWithNonSlicedCondition) { EXPECT_EQ(10, curInterval.value.long_value); valueProducer->onConditionChanged(false, bucket2StartTimeNs + 1); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs - 8}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs - 8}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); // has one slice EXPECT_EQ(1UL, valueProducer->mCurrentSlicedBucket.size()); @@ -636,10 +666,12 @@ TEST(ValueMetricProducerTest, TestEventsWithNonSlicedCondition) { EXPECT_EQ(false, curBaseInfo.hasBase); valueProducer->onConditionChanged(true, bucket3StartTimeNs + 1); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10, 20}, {bucketSizeNs - 8, 1}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10, 20}, {bucketSizeNs - 8, 1}, + {bucketStartTimeNs, bucket2StartTimeNs}, + {bucket2StartTimeNs, bucket3StartTimeNs}); } -TEST(ValueMetricProducerTest, TestPushedEventsWithUpgrade) { +TEST_P(ValueMetricProducerTest_PartialBucket, TestPushedEvents) { ValueMetric metric = ValueMetricProducerTestHelper::createMetric(); UidMap uidMap; @@ -660,25 +692,46 @@ TEST(ValueMetricProducerTest, TestPushedEventsWithUpgrade) { valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1); EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size()); - valueProducer.notifyAppUpgrade(bucketStartTimeNs + 150, "ANY.APP", 1, 1); - EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ(bucketStartTimeNs + 150, valueProducer.mCurrentBucketStartTimeNs); + int64_t partialBucketSplitTimeNs = bucketStartTimeNs + 150; + switch (GetParam()) { + case APP_UPGRADE: + valueProducer.notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + valueProducer.onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10}, + {partialBucketSplitTimeNs - bucketStartTimeNs}, + {bucketStartTimeNs}, {partialBucketSplitTimeNs}); + EXPECT_EQ(partialBucketSplitTimeNs, valueProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(0, valueProducer.getCurrentBucketNum()); + // Event arrives after the bucket split. LogEvent event2(/*uid=*/0, /*pid=*/0); - CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 59 * NS_PER_SEC, 10); + CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 59 * NS_PER_SEC, 20); valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event2); - EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ(bucketStartTimeNs + 150, valueProducer.mCurrentBucketStartTimeNs); + + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10}, + {partialBucketSplitTimeNs - bucketStartTimeNs}, + {bucketStartTimeNs}, {partialBucketSplitTimeNs}); + EXPECT_EQ(partialBucketSplitTimeNs, valueProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(0, valueProducer.getCurrentBucketNum()); // Next value should create a new bucket. LogEvent event3(/*uid=*/0, /*pid=*/0); - CreateRepeatedValueLogEvent(&event3, tagId, bucketStartTimeNs + 65 * NS_PER_SEC, 10); + CreateRepeatedValueLogEvent(&event3, tagId, bucket2StartTimeNs + 5 * NS_PER_SEC, 10); valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event3); - EXPECT_EQ(2UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10, 20}, + {partialBucketSplitTimeNs - bucketStartTimeNs, + bucket2StartTimeNs - partialBucketSplitTimeNs}, + {bucketStartTimeNs, partialBucketSplitTimeNs}, + {partialBucketSplitTimeNs, bucket2StartTimeNs}); EXPECT_EQ(bucketStartTimeNs + bucketSizeNs, valueProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(1, valueProducer.getCurrentBucketNum()); } -TEST(ValueMetricProducerTest, TestPulledValueWithUpgrade) { +TEST_P(ValueMetricProducerTest_PartialBucket, TestPulledValue) { ValueMetric metric = ValueMetricProducerTestHelper::createMetric(); UidMap uidMap; @@ -689,14 +742,16 @@ TEST(ValueMetricProducerTest, TestPulledValueWithUpgrade) { atomMatcherId, logEventMatcherIndex, atomMatcher, uidMap)}); sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>(); + int64_t partialBucketSplitTimeNs = bucket2StartTimeNs + 150; EXPECT_CALL(*pullerManager, RegisterReceiver(tagId, kConfigKey, _, _, _)).WillOnce(Return()); EXPECT_CALL(*pullerManager, UnRegisterReceiver(tagId, kConfigKey, _)).WillOnce(Return()); EXPECT_CALL(*pullerManager, Pull(tagId, kConfigKey, _, _)) .WillOnce(Return(true)) - .WillOnce(Invoke([](int tagId, const ConfigKey&, - vector<std::shared_ptr<LogEvent>>* data, bool) { + .WillOnce(Invoke([partialBucketSplitTimeNs](int tagId, const ConfigKey&, + vector<std::shared_ptr<LogEvent>>* data, + bool) { data->clear(); - data->push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 149, 120)); + data->push_back(CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs, 120)); return true; })); ValueMetricProducer valueProducer(kConfigKey, metric, -1, wizard, logEventMatcherIndex, @@ -711,20 +766,27 @@ TEST(ValueMetricProducerTest, TestPulledValueWithUpgrade) { valueProducer.onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size()); - valueProducer.notifyAppUpgrade(bucket2StartTimeNs + 150, "ANY.APP", 1, 1); - EXPECT_EQ(1UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ(bucket2StartTimeNs + 150, valueProducer.mCurrentBucketStartTimeNs); - assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20}, {150}); + switch (GetParam()) { + case APP_UPGRADE: + valueProducer.notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + valueProducer.onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } + EXPECT_EQ(partialBucketSplitTimeNs, valueProducer.mCurrentBucketStartTimeNs); + EXPECT_EQ(1, valueProducer.getCurrentBucketNum()); + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20}, {150}, {bucket2StartTimeNs}, + {partialBucketSplitTimeNs}); allData.clear(); allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket3StartTimeNs + 1, 150)); valueProducer.onDataPulled(allData, /** succeed */ true, bucket3StartTimeNs); - EXPECT_EQ(2UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); EXPECT_EQ(bucket3StartTimeNs, valueProducer.mCurrentBucketStartTimeNs); - EXPECT_EQ(20L, - valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].values[0].long_value); - assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20, 30}, - {150, bucketSizeNs - 150}); + EXPECT_EQ(2, valueProducer.getCurrentBucketNum()); + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20, 30}, {150, bucketSizeNs - 150}, + {bucket2StartTimeNs, partialBucketSplitTimeNs}, + {partialBucketSplitTimeNs, bucket3StartTimeNs}); } TEST(ValueMetricProducerTest, TestPulledWithAppUpgradeDisabled) { @@ -754,12 +816,12 @@ TEST(ValueMetricProducerTest, TestPulledWithAppUpgradeDisabled) { valueProducer.onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size()); - valueProducer.notifyAppUpgrade(bucket2StartTimeNs + 150, "ANY.APP", 1, 1); + valueProducer.notifyAppUpgrade(bucket2StartTimeNs + 150); EXPECT_EQ(0UL, valueProducer.mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); EXPECT_EQ(bucket2StartTimeNs, valueProducer.mCurrentBucketStartTimeNs); } -TEST(ValueMetricProducerTest, TestPulledValueWithUpgradeWhileConditionFalse) { +TEST_P(ValueMetricProducerTest_PartialBucket, TestPulledValueWhileConditionFalse) { ValueMetric metric = ValueMetricProducerTestHelper::createMetricWithCondition(); sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>(); @@ -784,14 +846,21 @@ TEST(ValueMetricProducerTest, TestPulledValueWithUpgradeWhileConditionFalse) { valueProducer->onConditionChanged(false, bucket2StartTimeNs - 100); EXPECT_FALSE(valueProducer->mCondition); - valueProducer->notifyAppUpgrade(bucket2StartTimeNs - 50, "ANY.APP", 1, 1); + int64_t partialBucketSplitTimeNs = bucket2StartTimeNs - 50; + switch (GetParam()) { + case APP_UPGRADE: + valueProducer->notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + valueProducer->onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } // Expect one full buckets already done and starting a partial bucket. - EXPECT_EQ(bucket2StartTimeNs - 50, valueProducer->mCurrentBucketStartTimeNs); - EXPECT_EQ(1UL, valueProducer->mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY].size()); - EXPECT_EQ(bucketStartTimeNs, - valueProducer->mPastBuckets[DEFAULT_METRIC_DIMENSION_KEY][0].mBucketStartNs); + EXPECT_EQ(partialBucketSplitTimeNs, valueProducer->mCurrentBucketStartTimeNs); + EXPECT_EQ(0, valueProducer->getCurrentBucketNum()); assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, - {(bucket2StartTimeNs - 100) - (bucketStartTimeNs + 1)}); + {(bucket2StartTimeNs - 100) - (bucketStartTimeNs + 1)}, + {bucketStartTimeNs}, {partialBucketSplitTimeNs}); EXPECT_FALSE(valueProducer->mCondition); } TEST(ValueMetricProducerTest, TestPushedEventsWithoutCondition) { @@ -834,7 +903,8 @@ TEST(ValueMetricProducerTest, TestPushedEventsWithoutCondition) { EXPECT_EQ(30, curInterval.value.long_value); valueProducer.flushIfNeededLocked(bucket2StartTimeNs); - assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {30}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {30}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestPushedEventsWithCondition) { @@ -895,7 +965,8 @@ TEST(ValueMetricProducerTest, TestPushedEventsWithCondition) { EXPECT_EQ(50, curInterval.value.long_value); valueProducer.flushIfNeededLocked(bucket2StartTimeNs); - assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {50}, {20}); + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {50}, {20}, {bucketStartTimeNs}, + {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestAnomalyDetection) { @@ -1012,7 +1083,8 @@ TEST(ValueMetricProducerTest, TestBucketBoundaryNoCondition) { EXPECT_EQ(true, curBaseInfo.hasBase); EXPECT_EQ(23, curBaseInfo.base.long_value); EXPECT_EQ(false, curInterval.hasValue); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {12}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {12}, {bucketSizeNs}, + {bucket2StartTimeNs}, {bucket3StartTimeNs}); // pull 3 come late. // The previous bucket gets closed with error. (Has start value 23, no ending) @@ -1028,7 +1100,15 @@ TEST(ValueMetricProducerTest, TestBucketBoundaryNoCondition) { EXPECT_EQ(true, curBaseInfo.hasBase); EXPECT_EQ(36, curBaseInfo.base.long_value); EXPECT_EQ(false, curInterval.hasValue); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {12}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {12}, {bucketSizeNs}, + {bucket2StartTimeNs}, {bucket3StartTimeNs}); + // The 3rd bucket is dropped due to multiple buckets being skipped. + ASSERT_EQ(1, valueProducer->mSkippedBuckets.size()); + EXPECT_EQ(bucket3StartTimeNs, valueProducer->mSkippedBuckets[0].bucketStartTimeNs); + EXPECT_EQ(bucket4StartTimeNs, valueProducer->mSkippedBuckets[0].bucketEndTimeNs); + ASSERT_EQ(1, valueProducer->mSkippedBuckets[0].dropEvents.size()); + EXPECT_EQ(MULTIPLE_BUCKETS_SKIPPED, valueProducer->mSkippedBuckets[0].dropEvents[0].reason); + EXPECT_EQ(bucket6StartTimeNs, valueProducer->mSkippedBuckets[0].dropEvents[0].dropTimeNs); } /* @@ -1073,7 +1153,8 @@ TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition) { valueProducer->onConditionChanged(false, bucket2StartTimeNs + 1); curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0]; curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0]; - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); EXPECT_EQ(false, curBaseInfo.hasBase); // Now the alarm is delivered. @@ -1082,7 +1163,8 @@ TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition) { allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 30, 110)); valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0]; curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0]; EXPECT_EQ(false, curBaseInfo.hasBase); @@ -1090,10 +1172,9 @@ TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition) { } /* -* Test pulled event with non sliced condition. The pull on boundary come late, after the -condition -* change to false, and then true again. This is due to alarm delivered late. -*/ + * Test pulled event with non sliced condition. The pull on boundary come late, after the condition + * change to false, and then true again. This is due to alarm delivered late. + */ TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition2) { ValueMetric metric = ValueMetricProducerTestHelper::createMetricWithCondition(); @@ -1139,7 +1220,8 @@ TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition2) { // pull on bucket boundary come late, condition change happens before it valueProducer->onConditionChanged(false, bucket2StartTimeNs + 1); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); EXPECT_EQ(1UL, valueProducer->mCurrentSlicedBucket.size()); curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0]; curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0]; @@ -1148,7 +1230,8 @@ TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition2) { // condition changed to true again, before the pull alarm is delivered valueProducer->onConditionChanged(true, bucket2StartTimeNs + 25); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0]; curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0]; EXPECT_EQ(true, curBaseInfo.hasBase); @@ -1167,13 +1250,15 @@ TEST(ValueMetricProducerTest, TestBucketBoundaryWithCondition2) { EXPECT_EQ(140, curBaseInfo.base.long_value); EXPECT_EQ(true, curInterval.hasValue); EXPECT_EQ(10, curInterval.value.long_value); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {bucketSizeNs - 8}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); allData.clear(); allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket3StartTimeNs, 160)); valueProducer->onDataPulled(allData, /** succeed */ true, bucket3StartTimeNs); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20, 30}, - {bucketSizeNs - 8, bucketSizeNs - 24}); + assertPastBucketValuesSingleKey( + valueProducer->mPastBuckets, {20, 30}, {bucketSizeNs - 8, bucketSizeNs - 24}, + {bucketStartTimeNs, bucket2StartTimeNs}, {bucket2StartTimeNs, bucket3StartTimeNs}); } TEST(ValueMetricProducerTest, TestPushedAggregateMin) { @@ -1216,7 +1301,8 @@ TEST(ValueMetricProducerTest, TestPushedAggregateMin) { EXPECT_EQ(10, curInterval.value.long_value); valueProducer.flushIfNeededLocked(bucket2StartTimeNs); - assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {10}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestPushedAggregateMax) { @@ -1239,9 +1325,6 @@ TEST(ValueMetricProducerTest, TestPushedAggregateMax) { LogEvent event1(/*uid=*/0, /*pid=*/0); CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10); - - LogEvent event2(/*uid=*/0, /*pid=*/0); - CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 20); valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1); // has one slice @@ -1251,6 +1334,8 @@ TEST(ValueMetricProducerTest, TestPushedAggregateMax) { EXPECT_EQ(10, curInterval.value.long_value); EXPECT_EQ(true, curInterval.hasValue); + LogEvent event2(/*uid=*/0, /*pid=*/0); + CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 20, 20); valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event2); // has one slice @@ -1258,10 +1343,9 @@ TEST(ValueMetricProducerTest, TestPushedAggregateMax) { curInterval = valueProducer.mCurrentSlicedBucket.begin()->second[0]; EXPECT_EQ(20, curInterval.value.long_value); - valueProducer.flushIfNeededLocked(bucket3StartTimeNs); - /* EXPECT_EQ(1UL, valueProducer.mPastBuckets.size()); */ - /* EXPECT_EQ(1UL, valueProducer.mPastBuckets.begin()->second.size()); */ - /* EXPECT_EQ(20, valueProducer.mPastBuckets.begin()->second.back().values[0].long_value); */ + valueProducer.flushIfNeededLocked(bucket2StartTimeNs); + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {20}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestPushedAggregateAvg) { @@ -1351,7 +1435,8 @@ TEST(ValueMetricProducerTest, TestPushedAggregateSum) { EXPECT_EQ(25, curInterval.value.long_value); valueProducer.flushIfNeededLocked(bucket2StartTimeNs); - assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {25}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {25}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestSkipZeroDiffOutput) { @@ -1375,10 +1460,8 @@ TEST(ValueMetricProducerTest, TestSkipZeroDiffOutput) { LogEvent event1(/*uid=*/0, /*pid=*/0); CreateRepeatedValueLogEvent(&event1, tagId, bucketStartTimeNs + 10, 10); - - LogEvent event2(/*uid=*/0, /*pid=*/0); - CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 15, 15); valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event1); + // has one slice EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size()); ValueMetricProducer::Interval curInterval = @@ -1388,6 +1471,8 @@ TEST(ValueMetricProducerTest, TestSkipZeroDiffOutput) { EXPECT_EQ(10, curBaseInfo.base.long_value); EXPECT_EQ(false, curInterval.hasValue); + LogEvent event2(/*uid=*/0, /*pid=*/0); + CreateRepeatedValueLogEvent(&event2, tagId, bucketStartTimeNs + 15, 15); valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event2); // has one slice @@ -1400,12 +1485,14 @@ TEST(ValueMetricProducerTest, TestSkipZeroDiffOutput) { LogEvent event3(/*uid=*/0, /*pid=*/0); CreateRepeatedValueLogEvent(&event3, tagId, bucket2StartTimeNs + 10, 15); valueProducer.onMatchedLogEvent(1 /*log matcher index*/, event3); + EXPECT_EQ(1UL, valueProducer.mCurrentSlicedBucket.size()); curInterval = valueProducer.mCurrentSlicedBucket.begin()->second[0]; curBaseInfo = valueProducer.mCurrentBaseInfo.begin()->second[0]; EXPECT_EQ(true, curBaseInfo.hasBase); EXPECT_EQ(15, curBaseInfo.base.long_value); EXPECT_EQ(true, curInterval.hasValue); + EXPECT_EQ(0, curInterval.value.long_value); LogEvent event4(/*uid=*/0, /*pid=*/0); CreateRepeatedValueLogEvent(&event4, tagId, bucket2StartTimeNs + 15, 15); @@ -1416,11 +1503,11 @@ TEST(ValueMetricProducerTest, TestSkipZeroDiffOutput) { EXPECT_EQ(true, curBaseInfo.hasBase); EXPECT_EQ(15, curBaseInfo.base.long_value); EXPECT_EQ(true, curInterval.hasValue); + EXPECT_EQ(0, curInterval.value.long_value); valueProducer.flushIfNeededLocked(bucket3StartTimeNs); - EXPECT_EQ(1UL, valueProducer.mPastBuckets.size()); - EXPECT_EQ(1UL, valueProducer.mPastBuckets.begin()->second.size()); - assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {5}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer.mPastBuckets, {5}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestSkipZeroDiffOutputMultiValue) { @@ -1740,8 +1827,8 @@ TEST(ValueMetricProducerTest, TestTrimUnusedDimensionKey) { EXPECT_EQ(3, baseInfo1.base.long_value); EXPECT_EQ(false, interval1.hasValue); EXPECT_EQ(0UL, valueProducer->mPastBuckets.size()); - vector<shared_ptr<LogEvent>> allData; + vector<shared_ptr<LogEvent>> allData; allData.clear(); allData.push_back(CreateTwoValueLogEvent(tagId, bucket2StartTimeNs + 1, 2, 4)); allData.push_back(CreateTwoValueLogEvent(tagId, bucket2StartTimeNs + 1, 1, 11)); @@ -1753,7 +1840,8 @@ TEST(ValueMetricProducerTest, TestTrimUnusedDimensionKey) { EXPECT_EQ(false, interval1.hasValue); EXPECT_EQ(8, interval1.value.long_value); EXPECT_FALSE(interval1.seenNewData); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); auto it = valueProducer->mCurrentSlicedBucket.begin(); for (; it != valueProducer->mCurrentSlicedBucket.end(); it++) { @@ -1769,14 +1857,13 @@ TEST(ValueMetricProducerTest, TestTrimUnusedDimensionKey) { } EXPECT_TRUE(it != iter); EXPECT_TRUE(itBase != iterBase); - auto& interval2 = it->second[0]; - auto& baseInfo2 = itBase->second[0]; + auto interval2 = it->second[0]; + auto baseInfo2 = itBase->second[0]; EXPECT_EQ(2, it->first.getDimensionKeyInWhat().getValues()[0].mValue.int_value); EXPECT_EQ(true, baseInfo2.hasBase); EXPECT_EQ(4, baseInfo2.base.long_value); EXPECT_EQ(false, interval2.hasValue); EXPECT_FALSE(interval2.seenNewData); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs}); // next pull somehow did not happen, skip to end of bucket 3 allData.clear(); @@ -1791,7 +1878,8 @@ TEST(ValueMetricProducerTest, TestTrimUnusedDimensionKey) { EXPECT_EQ(5, baseInfo2.base.long_value); EXPECT_EQ(false, interval2.hasValue); EXPECT_FALSE(interval2.seenNewData); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {8}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); allData.clear(); allData.push_back(CreateTwoValueLogEvent(tagId, bucket5StartTimeNs + 1, 2, 14)); @@ -1805,9 +1893,13 @@ TEST(ValueMetricProducerTest, TestTrimUnusedDimensionKey) { EXPECT_FALSE(interval2.seenNewData); ASSERT_EQ(2UL, valueProducer->mPastBuckets.size()); auto iterator = valueProducer->mPastBuckets.begin(); + EXPECT_EQ(bucket4StartTimeNs, iterator->second[0].mBucketStartNs); + EXPECT_EQ(bucket5StartTimeNs, iterator->second[0].mBucketEndNs); EXPECT_EQ(9, iterator->second[0].values[0].long_value); EXPECT_EQ(bucketSizeNs, iterator->second[0].mConditionTrueNs); iterator++; + EXPECT_EQ(bucketStartTimeNs, iterator->second[0].mBucketStartNs); + EXPECT_EQ(bucket2StartTimeNs, iterator->second[0].mBucketEndNs); EXPECT_EQ(8, iterator->second[0].values[0].long_value); EXPECT_EQ(bucketSizeNs, iterator->second[0].mConditionTrueNs); } @@ -2414,7 +2506,8 @@ TEST(ValueMetricProducerTest, TestEmptyDataResetsBase_onBucketBoundary) { EXPECT_EQ(true, valueProducer->mHasGlobalBase); EXPECT_EQ(1UL, valueProducer->mPastBuckets.size()); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {1}, {bucketSizeNs - 12 + 1}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {1}, {bucketSizeNs - 12 + 1}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestPartialResetOnBucketBoundaries) { @@ -2461,10 +2554,11 @@ TEST(ValueMetricProducerTest, TestPartialResetOnBucketBoundaries) { EXPECT_EQ(true, valueProducer->mHasGlobalBase); } -TEST(ValueMetricProducerTest, TestFullBucketResetWhenLastBucketInvalid) { +TEST_P(ValueMetricProducerTest_PartialBucket, TestFullBucketResetWhenLastBucketInvalid) { ValueMetric metric = ValueMetricProducerTestHelper::createMetric(); sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>(); + int64_t partialBucketSplitTimeNs = bucketStartTimeNs + bucketSizeNs / 2; EXPECT_CALL(*pullerManager, Pull(tagId, kConfigKey, _, _)) // Initialization. .WillOnce(Invoke( @@ -2474,23 +2568,41 @@ TEST(ValueMetricProducerTest, TestFullBucketResetWhenLastBucketInvalid) { return true; })) // notifyAppUpgrade. - .WillOnce(Invoke([](int tagId, const ConfigKey&, - vector<std::shared_ptr<LogEvent>>* data, bool) { + .WillOnce(Invoke([partialBucketSplitTimeNs](int tagId, const ConfigKey&, + vector<std::shared_ptr<LogEvent>>* data, + bool) { data->clear(); - data->push_back(CreateRepeatedValueLogEvent( - tagId, bucketStartTimeNs + bucketSizeNs / 2, 10)); + data->push_back(CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs, 10)); return true; })); sp<ValueMetricProducer> valueProducer = ValueMetricProducerTestHelper::createValueProducerNoConditions(pullerManager, metric); ASSERT_EQ(0UL, valueProducer->mCurrentFullBucket.size()); - valueProducer->notifyAppUpgrade(bucketStartTimeNs + bucketSizeNs / 2, "com.foo", 10000, 1); + switch (GetParam()) { + case APP_UPGRADE: + valueProducer->notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + valueProducer->onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } + EXPECT_EQ(partialBucketSplitTimeNs, valueProducer->mCurrentBucketStartTimeNs); + EXPECT_EQ(0, valueProducer->getCurrentBucketNum()); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {9}, + {partialBucketSplitTimeNs - bucketStartTimeNs}, + {bucketStartTimeNs}, {partialBucketSplitTimeNs}); ASSERT_EQ(1UL, valueProducer->mCurrentFullBucket.size()); vector<shared_ptr<LogEvent>> allData; allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket3StartTimeNs + 1, 4)); valueProducer->onDataPulled(allData, /** fails */ false, bucket3StartTimeNs + 1); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {9}, + {partialBucketSplitTimeNs - bucketStartTimeNs}, + {bucketStartTimeNs}, {partialBucketSplitTimeNs}); + ASSERT_EQ(1, valueProducer->mSkippedBuckets.size()); + EXPECT_EQ(partialBucketSplitTimeNs, valueProducer->mSkippedBuckets[0].bucketStartTimeNs); + EXPECT_EQ(bucket2StartTimeNs, valueProducer->mSkippedBuckets[0].bucketEndTimeNs); ASSERT_EQ(0UL, valueProducer->mCurrentFullBucket.size()); } @@ -2537,7 +2649,8 @@ TEST(ValueMetricProducerTest, TestBucketBoundariesOnConditionChange) { valueProducer->onConditionChanged(false, bucket3StartTimeNs + 10); // Bucket should have been completed. - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {2}, {bucketSizeNs - 10}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {2}, {bucketSizeNs - 10}, + {bucket2StartTimeNs}, {bucket3StartTimeNs}); } TEST(ValueMetricProducerTest, TestLateOnDataPulledWithoutDiff) { @@ -2557,7 +2670,8 @@ TEST(ValueMetricProducerTest, TestLateOnDataPulledWithoutDiff) { valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); // Bucket should have been completed. - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {30}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {30}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestLateOnDataPulledWithDiff) { @@ -2585,12 +2699,14 @@ TEST(ValueMetricProducerTest, TestLateOnDataPulledWithDiff) { valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); // Bucket should have been completed. - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {19}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {19}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } -TEST(ValueMetricProducerTest, TestBucketBoundariesOnAppUpgrade) { +TEST_P(ValueMetricProducerTest_PartialBucket, TestBucketBoundariesOnPartialBucket) { ValueMetric metric = ValueMetricProducerTestHelper::createMetric(); + int64_t partialBucketSplitTimeNs = bucket2StartTimeNs + 2; sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>(); EXPECT_CALL(*pullerManager, Pull(tagId, kConfigKey, _, _)) // Initialization. @@ -2601,20 +2717,29 @@ TEST(ValueMetricProducerTest, TestBucketBoundariesOnAppUpgrade) { return true; })) // notifyAppUpgrade. - .WillOnce(Invoke([](int tagId, const ConfigKey&, - vector<std::shared_ptr<LogEvent>>* data, bool) { + .WillOnce(Invoke([partialBucketSplitTimeNs](int tagId, const ConfigKey&, + vector<std::shared_ptr<LogEvent>>* data, + bool) { data->clear(); - data->push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 2, 10)); + data->push_back(CreateRepeatedValueLogEvent(tagId, partialBucketSplitTimeNs, 10)); return true; })); sp<ValueMetricProducer> valueProducer = ValueMetricProducerTestHelper::createValueProducerNoConditions(pullerManager, metric); - valueProducer->notifyAppUpgrade(bucket2StartTimeNs + 2, "com.foo", 10000, 1); + switch (GetParam()) { + case APP_UPGRADE: + valueProducer->notifyAppUpgrade(partialBucketSplitTimeNs); + break; + case BOOT_COMPLETE: + valueProducer->onStatsdInitCompleted(partialBucketSplitTimeNs); + break; + } // Bucket should have been completed. - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {9}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {9}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestDataIsNotUpdatedWhenNoConditionChanged) { @@ -2642,7 +2767,7 @@ TEST(ValueMetricProducerTest, TestDataIsNotUpdatedWhenNoConditionChanged) { valueProducer->onConditionChanged(true, bucketStartTimeNs + 8); valueProducer->onConditionChanged(false, bucketStartTimeNs + 10); - valueProducer->onConditionChanged(false, bucketStartTimeNs + 10); + valueProducer->onConditionChanged(false, bucketStartTimeNs + 12); EXPECT_EQ(1UL, valueProducer->mCurrentSlicedBucket.size()); auto curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0]; @@ -2654,7 +2779,8 @@ TEST(ValueMetricProducerTest, TestDataIsNotUpdatedWhenNoConditionChanged) { allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 1, 10)); valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs + 1); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {2}, {2}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {2}, {2}, {bucketStartTimeNs}, + {bucket2StartTimeNs}); } // TODO: b/145705635 fix or delete this test @@ -2705,7 +2831,7 @@ TEST(ValueMetricProducerTest, TestBucketInvalidIfGlobalBaseIsNotSet) { valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); // There was not global base available so all buckets are invalid. - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {}, {}, {}); } TEST(ValueMetricProducerTest, TestPullNeededFastDump) { @@ -2849,7 +2975,8 @@ TEST(ValueMetricProducerTest, TestPulledData_noDiff_withoutCondition) { valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs + 30); // Bucket should have been completed. - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {10}, {bucketSizeNs}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); } TEST(ValueMetricProducerTest, TestPulledData_noDiff_withMultipleConditionChanges) { @@ -2892,7 +3019,8 @@ TEST(ValueMetricProducerTest, TestPulledData_noDiff_withMultipleConditionChanges allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 30, 110)); valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {50 - 8}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {20}, {50 - 8}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0]; curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0]; EXPECT_EQ(false, curBaseInfo.hasBase); @@ -2923,7 +3051,8 @@ TEST(ValueMetricProducerTest, TestPulledData_noDiff_bucketBoundaryTrue) { allData.push_back(CreateRepeatedValueLogEvent(tagId, bucket2StartTimeNs + 30, 30)); valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {30}, {bucketSizeNs - 8}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {30}, {bucketSizeNs - 8}, + {bucketStartTimeNs}, {bucket2StartTimeNs}); ValueMetricProducer::Interval curInterval = valueProducer->mCurrentSlicedBucket.begin()->second[0]; ValueMetricProducer::BaseInfo curBaseInfo = valueProducer->mCurrentBaseInfo.begin()->second[0]; @@ -2946,7 +3075,7 @@ TEST(ValueMetricProducerTest, TestPulledData_noDiff_bucketBoundaryFalse) { valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); // Condition was always false. - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {}, {}, {}); } TEST(ValueMetricProducerTest, TestPulledData_noDiff_withFailure) { @@ -2976,7 +3105,7 @@ TEST(ValueMetricProducerTest, TestPulledData_noDiff_withFailure) { valueProducer->onDataPulled(allData, /** succeed */ true, bucket2StartTimeNs); // No buckets, we had a failure. - assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {}); + assertPastBucketValuesSingleKey(valueProducer->mPastBuckets, {}, {}, {}, {}); } /* diff --git a/cmds/statsd/tests/statsd_test_util.h b/cmds/statsd/tests/statsd_test_util.h index 37b98891797a..4d68ea2ecb79 100644 --- a/cmds/statsd/tests/statsd_test_util.h +++ b/cmds/statsd/tests/statsd_test_util.h @@ -42,6 +42,8 @@ using Status = ::ndk::ScopedAStatus; const int SCREEN_STATE_ATOM_ID = util::SCREEN_STATE_CHANGED; const int UID_PROCESS_STATE_ATOM_ID = util::UID_PROCESS_STATE_CHANGED; +enum BucketSplitEvent { APP_UPGRADE, BOOT_COMPLETE }; + // Converts a ProtoOutputStream to a StatsLogReport proto. StatsLogReport outputStreamToProto(ProtoOutputStream* proto); diff --git a/cmds/statsd/tests/utils/MultiConditionTrigger_test.cpp b/cmds/statsd/tests/utils/MultiConditionTrigger_test.cpp new file mode 100644 index 000000000000..db402a0dd658 --- /dev/null +++ b/cmds/statsd/tests/utils/MultiConditionTrigger_test.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2020 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. + */ +#include "utils/MultiConditionTrigger.h" + +#include <gtest/gtest.h> + +#include <chrono> +#include <set> +#include <thread> +#include <vector> + +#ifdef __ANDROID__ + +using namespace std; +using std::this_thread::sleep_for; + +namespace android { +namespace os { +namespace statsd { + +TEST(MultiConditionTrigger, TestMultipleConditions) { + int numConditions = 5; + string t1 = "t1", t2 = "t2", t3 = "t3", t4 = "t4", t5 = "t5"; + set<string> conditionNames = {t1, t2, t3, t4, t5}; + + mutex lock; + condition_variable cv; + bool triggerCalled = false; + + // Mark done as true and notify in the done. + MultiConditionTrigger trigger(conditionNames, [&lock, &cv, &triggerCalled] { + { + lock_guard lg(lock); + triggerCalled = true; + } + cv.notify_all(); + }); + + vector<thread> threads; + vector<bool> done(numConditions, false); + + int i = 0; + for (const string& conditionName : conditionNames) { + threads.emplace_back([&done, &conditionName, &trigger, i] { + sleep_for(chrono::milliseconds(3)); + done[i] = true; + trigger.markComplete(conditionName); + }); + i++; + } + + unique_lock<mutex> unique_lk(lock); + cv.wait(unique_lk, [&triggerCalled] { + return triggerCalled; + }); + + for (i = 0; i < numConditions; i++) { + EXPECT_EQ(done[i], 1); + } + + for (i = 0; i < numConditions; i++) { + threads[i].join(); + } +} + +TEST(MultiConditionTrigger, TestNoConditions) { + mutex lock; + condition_variable cv; + bool triggerCalled = false; + + MultiConditionTrigger trigger({}, [&lock, &cv, &triggerCalled] { + { + lock_guard lg(lock); + triggerCalled = true; + } + cv.notify_all(); + }); + + unique_lock<mutex> unique_lk(lock); + cv.wait(unique_lk, [&triggerCalled] { return triggerCalled; }); + EXPECT_TRUE(triggerCalled); + // Ensure that trigger occurs immediately if no events need to be completed. +} + +TEST(MultiConditionTrigger, TestMarkCompleteCalledBySameCondition) { + string t1 = "t1", t2 = "t2"; + set<string> conditionNames = {t1, t2}; + + mutex lock; + condition_variable cv; + bool triggerCalled = false; + + MultiConditionTrigger trigger(conditionNames, [&lock, &cv, &triggerCalled] { + { + lock_guard lg(lock); + triggerCalled = true; + } + cv.notify_all(); + }); + + trigger.markComplete(t1); + trigger.markComplete(t1); + + // Ensure that the trigger still hasn't fired. + { + lock_guard lg(lock); + EXPECT_FALSE(triggerCalled); + } + + trigger.markComplete(t2); + unique_lock<mutex> unique_lk(lock); + cv.wait(unique_lk, [&triggerCalled] { return triggerCalled; }); + EXPECT_TRUE(triggerCalled); +} + +TEST(MultiConditionTrigger, TestTriggerOnlyCalledOnce) { + string t1 = "t1"; + set<string> conditionNames = {t1}; + + mutex lock; + condition_variable cv; + bool triggerCalled = false; + int triggerCount = 0; + + MultiConditionTrigger trigger(conditionNames, [&lock, &cv, &triggerCalled, &triggerCount] { + { + lock_guard lg(lock); + triggerCount++; + triggerCalled = true; + } + cv.notify_all(); + }); + + trigger.markComplete(t1); + + // Ensure that the trigger fired. + { + unique_lock<mutex> unique_lk(lock); + cv.wait(unique_lk, [&triggerCalled] { return triggerCalled; }); + EXPECT_TRUE(triggerCalled); + EXPECT_EQ(triggerCount, 1); + triggerCalled = false; + } + + trigger.markComplete(t1); + + // Ensure that the trigger does not fire again. + { + unique_lock<mutex> unique_lk(lock); + cv.wait_for(unique_lk, chrono::milliseconds(5), [&triggerCalled] { return triggerCalled; }); + EXPECT_FALSE(triggerCalled); + EXPECT_EQ(triggerCount, 1); + } +} + +} // namespace statsd +} // namespace os +} // namespace android +#else +GTEST_LOG_(INFO) << "This test does nothing.\n"; +#endif diff --git a/cmds/statsd/tests/utils/NamedLatch_test.cpp b/cmds/statsd/tests/utils/NamedLatch_test.cpp deleted file mode 100644 index de48a133823e..000000000000 --- a/cmds/statsd/tests/utils/NamedLatch_test.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2020 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. - */ -#include "utils/NamedLatch.h" - -#include <gtest/gtest.h> - -#include <chrono> -#include <set> -#include <thread> -#include <vector> - -#ifdef __ANDROID__ - -using namespace std; -using std::this_thread::sleep_for; - -namespace android { -namespace os { -namespace statsd { - -TEST(NamedLatchTest, TestWait) { - int numEvents = 5; - string t1 = "t1", t2 = "t2", t3 = "t3", t4 = "t4", t5 = "t5"; - set<string> eventNames = {t1, t2, t3, t4, t5}; - - NamedLatch latch(eventNames); - vector<thread> threads; - vector<bool> done(numEvents, false); - - int i = 0; - for (const string& eventName : eventNames) { - threads.emplace_back([&done, &eventName, &latch, i] { - sleep_for(chrono::milliseconds(3)); - done[i] = true; - latch.countDown(eventName); - }); - i++; - } - - latch.wait(); - - for (i = 0; i < numEvents; i++) { - EXPECT_EQ(done[i], 1); - } - - for (i = 0; i < numEvents; i++) { - threads[i].join(); - } -} - -TEST(NamedLatchTest, TestNoWorkers) { - NamedLatch latch({}); - latch.wait(); - // Ensure that latch does not wait if no events need to countDown. -} - -TEST(NamedLatchTest, TestCountDownCalledBySameEventName) { - string t1 = "t1", t2 = "t2"; - set<string> eventNames = {t1, t2}; - - NamedLatch latch(eventNames); - - thread waiterThread([&latch] { latch.wait(); }); - - latch.countDown(t1); - latch.countDown(t1); - - // Ensure that the latch's remaining threads still has t2. - latch.mMutex.lock(); - ASSERT_EQ(latch.mRemainingEventNames.size(), 1); - EXPECT_NE(latch.mRemainingEventNames.find(t2), latch.mRemainingEventNames.end()); - latch.mMutex.unlock(); - - latch.countDown(t2); - waiterThread.join(); -} - -} // namespace statsd -} // namespace os -} // namespace android -#else -GTEST_LOG_(INFO) << "This test does nothing.\n"; -#endif diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 67334f554df7..18e5c3d2d7cc 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -494,6 +494,13 @@ public abstract class AccessibilityService extends Service { */ public static final int GLOBAL_ACTION_TAKE_SCREENSHOT = 9; + /** + * Action to send the KEYCODE_HEADSETHOOK KeyEvent, which is used to answer/hang up calls and + * play/stop media + * @hide + */ + public static final int GLOBAL_ACTION_KEYCODE_HEADSETHOOK = 10; + private static final String LOG_TAG = "AccessibilityService"; /** diff --git a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java index 9a3dad2eb92f..623734ee5662 100644 --- a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java @@ -30,7 +30,6 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; -import android.text.TextUtils; import android.util.AttributeSet; import android.util.Xml; @@ -153,7 +152,7 @@ public final class AccessibilityShortcutInfo { com.android.internal.R.styleable.AccessibilityShortcutTarget_settingsActivity); asAttributes.recycle(); - if (mDescriptionResId == 0 || mSummaryResId == 0) { + if ((mDescriptionResId == 0 && mHtmlDescriptionRes == 0) || mSummaryResId == 0) { throw new XmlPullParserException("No description or summary in meta-data"); } } catch (PackageManager.NameNotFoundException e) { @@ -243,7 +242,10 @@ public final class AccessibilityShortcutInfo { public String loadHtmlDescription(@NonNull PackageManager packageManager) { final String htmlDescription = loadResourceString(packageManager, mActivityInfo, mHtmlDescriptionRes); - return TextUtils.isEmpty(htmlDescription) ? null : getFilteredHtmlText(htmlDescription); + if (htmlDescription != null) { + return getFilteredHtmlText(htmlDescription); + } + return null; } /** diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java index 1b941defba42..1cc63da3db0a 100644 --- a/core/java/android/app/ActivityTaskManager.java +++ b/core/java/android/app/ActivityTaskManager.java @@ -23,8 +23,10 @@ import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -430,4 +432,14 @@ public class ActivityTaskManager { throw e.rethrowFromSystemServer(); } } + + /** Returns whether the current UI mode supports error dialogs (ANR, crash, etc). */ + public static boolean currentUiModeSupportsErrorDialogs(@NonNull Context context) { + final Configuration config = context.getResources().getConfiguration(); + int modeType = config.uiMode & Configuration.UI_MODE_TYPE_MASK; + return (modeType != Configuration.UI_MODE_TYPE_CAR + && !(modeType == Configuration.UI_MODE_TYPE_WATCH && Build.IS_USER) + && modeType != Configuration.UI_MODE_TYPE_TELEVISION + && modeType != Configuration.UI_MODE_TYPE_VR_HEADSET); + } } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 8edf03d033f8..af027837ec91 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -2301,11 +2301,11 @@ public class Notification implements Parcelable priority = parcel.readInt(); - category = parcel.readString(); + category = parcel.readString8(); - mGroupKey = parcel.readString(); + mGroupKey = parcel.readString8(); - mSortKey = parcel.readString(); + mSortKey = parcel.readString8(); extras = Bundle.setDefusable(parcel.readBundle(), true); // may be null fixDuplicateExtras(); @@ -2329,12 +2329,12 @@ public class Notification implements Parcelable color = parcel.readInt(); if (parcel.readInt() != 0) { - mChannelId = parcel.readString(); + mChannelId = parcel.readString8(); } mTimeout = parcel.readLong(); if (parcel.readInt() != 0) { - mShortcutId = parcel.readString(); + mShortcutId = parcel.readString8(); } if (parcel.readInt() != 0) { @@ -2766,11 +2766,11 @@ public class Notification implements Parcelable parcel.writeInt(priority); - parcel.writeString(category); + parcel.writeString8(category); - parcel.writeString(mGroupKey); + parcel.writeString8(mGroupKey); - parcel.writeString(mSortKey); + parcel.writeString8(mSortKey); parcel.writeBundle(extras); // null ok @@ -2803,7 +2803,7 @@ public class Notification implements Parcelable if (mChannelId != null) { parcel.writeInt(1); - parcel.writeString(mChannelId); + parcel.writeString8(mChannelId); } else { parcel.writeInt(0); } @@ -2811,7 +2811,7 @@ public class Notification implements Parcelable if (mShortcutId != null) { parcel.writeInt(1); - parcel.writeString(mShortcutId); + parcel.writeString8(mShortcutId); } else { parcel.writeInt(0); } @@ -8873,7 +8873,7 @@ public class Notification implements Parcelable } mDesiredHeightResId = in.readInt(); if (in.readInt() != 0) { - mShortcutId = in.readString(); + mShortcutId = in.readString8(); } } @@ -9029,7 +9029,7 @@ public class Notification implements Parcelable out.writeInt(mDesiredHeightResId); out.writeInt(TextUtils.isEmpty(mShortcutId) ? 0 : 1); if (!TextUtils.isEmpty(mShortcutId)) { - out.writeString(mShortcutId); + out.writeString8(mShortcutId); } } diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index 2feb9277775c..9f8d3c4090d6 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -102,7 +102,7 @@ public final class NotificationChannel implements Parcelable { private static final String ATT_FG_SERVICE_SHOWN = "fgservice"; private static final String ATT_GROUP = "group"; private static final String ATT_BLOCKABLE_SYSTEM = "blockable_system"; - private static final String ATT_ALLOW_BUBBLE = "allow_bubble"; + private static final String ATT_ALLOW_BUBBLE = "allow_bubbles"; private static final String ATT_ORIG_IMP = "orig_imp"; private static final String ATT_PARENT_CHANNEL = "parent"; private static final String ATT_CONVERSATION_ID = "conv_id"; @@ -168,7 +168,12 @@ public final class NotificationChannel implements Parcelable { NotificationManager.IMPORTANCE_UNSPECIFIED; private static final boolean DEFAULT_DELETED = false; private static final boolean DEFAULT_SHOW_BADGE = true; - private static final boolean DEFAULT_ALLOW_BUBBLE = false; + /** + * @hide + */ + public static final int DEFAULT_ALLOW_BUBBLE = -1; + private static final int ALLOW_BUBBLE_ON = 1; + private static final int ALLOW_BUBBLE_OFF = 0; @UnsupportedAppUsage private String mId; @@ -193,7 +198,7 @@ public final class NotificationChannel implements Parcelable { private AudioAttributes mAudioAttributes = Notification.AUDIO_ATTRIBUTES_DEFAULT; // If this is a blockable system notification channel. private boolean mBlockableSystem = false; - private boolean mAllowBubbles = DEFAULT_ALLOW_BUBBLE; + private int mAllowBubbles = DEFAULT_ALLOW_BUBBLE; private boolean mImportanceLockedByOEM; private boolean mImportanceLockedDefaultApp; private String mParentId = null; @@ -261,7 +266,7 @@ public final class NotificationChannel implements Parcelable { mAudioAttributes = in.readInt() > 0 ? AudioAttributes.CREATOR.createFromParcel(in) : null; mLightColor = in.readInt(); mBlockableSystem = in.readBoolean(); - mAllowBubbles = in.readBoolean(); + mAllowBubbles = in.readInt(); mImportanceLockedByOEM = in.readBoolean(); mOriginalImportance = in.readInt(); mParentId = in.readString(); @@ -320,7 +325,7 @@ public final class NotificationChannel implements Parcelable { } dest.writeInt(mLightColor); dest.writeBoolean(mBlockableSystem); - dest.writeBoolean(mAllowBubbles); + dest.writeInt(mAllowBubbles); dest.writeBoolean(mImportanceLockedByOEM); dest.writeInt(mOriginalImportance); dest.writeString(mParentId); @@ -550,7 +555,14 @@ public final class NotificationChannel implements Parcelable { * @see Notification#getBubbleMetadata() */ public void setAllowBubbles(boolean allowBubbles) { - mAllowBubbles = allowBubbles; + mAllowBubbles = allowBubbles ? ALLOW_BUBBLE_ON : ALLOW_BUBBLE_OFF; + } + + /** + * @hide + */ + public void setAllowBubbles(int allowed) { + mAllowBubbles = allowed; } /** @@ -701,6 +713,13 @@ public final class NotificationChannel implements Parcelable { * @see Notification#getBubbleMetadata() */ public boolean canBubble() { + return mAllowBubbles == ALLOW_BUBBLE_ON; + } + + /** + * @hide + */ + public int getAllowBubbles() { return mAllowBubbles; } @@ -872,7 +891,7 @@ public final class NotificationChannel implements Parcelable { lockFields(safeInt(parser, ATT_USER_LOCKED, 0)); setFgServiceShown(safeBool(parser, ATT_FG_SERVICE_SHOWN, false)); setBlockable(safeBool(parser, ATT_BLOCKABLE_SYSTEM, false)); - setAllowBubbles(safeBool(parser, ATT_ALLOW_BUBBLE, DEFAULT_ALLOW_BUBBLE)); + setAllowBubbles(safeInt(parser, ATT_ALLOW_BUBBLE, DEFAULT_ALLOW_BUBBLE)); setOriginalImportance(safeInt(parser, ATT_ORIG_IMP, DEFAULT_IMPORTANCE)); setConversationId(parser.getAttributeValue(null, ATT_PARENT_CHANNEL), parser.getAttributeValue(null, ATT_CONVERSATION_ID)); @@ -996,8 +1015,8 @@ public final class NotificationChannel implements Parcelable { if (isBlockable()) { out.attribute(null, ATT_BLOCKABLE_SYSTEM, Boolean.toString(isBlockable())); } - if (canBubble() != DEFAULT_ALLOW_BUBBLE) { - out.attribute(null, ATT_ALLOW_BUBBLE, Boolean.toString(canBubble())); + if (getAllowBubbles() != DEFAULT_ALLOW_BUBBLE) { + out.attribute(null, ATT_ALLOW_BUBBLE, Integer.toString(getAllowBubbles())); } if (getOriginalImportance() != DEFAULT_IMPORTANCE) { out.attribute(null, ATT_ORIG_IMP, Integer.toString(getOriginalImportance())); @@ -1059,7 +1078,7 @@ public final class NotificationChannel implements Parcelable { record.put(ATT_DELETED, Boolean.toString(isDeleted())); record.put(ATT_GROUP, getGroup()); record.put(ATT_BLOCKABLE_SYSTEM, isBlockable()); - record.put(ATT_ALLOW_BUBBLE, canBubble()); + record.put(ATT_ALLOW_BUBBLE, getAllowBubbles()); // TODO: original importance return record; } diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index 0173731995dd..c7a2a1e11c9e 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -20,6 +20,7 @@ import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Intent; @@ -196,6 +197,20 @@ public class TaskInfo { return resizeMode != RESIZE_MODE_UNRESIZEABLE; } + /** @hide */ + @NonNull + @TestApi + public WindowContainerToken getToken() { + return token; + } + + /** @hide */ + @NonNull + @TestApi + public Configuration getConfiguration() { + return configuration; + } + /** * Reads the TaskInfo from a parcel. */ diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index 9c806fa286bc..1923bf3c5a23 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -1206,7 +1206,7 @@ public class ClipData implements Parcelable { } } else { dest.writeInt(PARCEL_TYPE_STRING); - dest.writeString(text); + dest.writeString8(text); } } @@ -1215,7 +1215,7 @@ public class ClipData implements Parcelable { */ private static String readHtmlTextFromParcel(Parcel in) { if (in.readInt() == PARCEL_TYPE_STRING) { - return in.readString(); + return in.readString8(); } ParcelFileDescriptor pfd = in.readParcelable(ParcelFileDescriptor.class.getClassLoader()); diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java index 494d2aeaf42a..1fb426eb3cfc 100644 --- a/core/java/android/content/ContentProviderOperation.java +++ b/core/java/android/content/ContentProviderOperation.java @@ -91,8 +91,8 @@ public class ContentProviderOperation implements Parcelable { private ContentProviderOperation(Parcel source) { mType = source.readInt(); mUri = Uri.CREATOR.createFromParcel(source); - mMethod = source.readInt() != 0 ? source.readString() : null; - mArg = source.readInt() != 0 ? source.readString() : null; + mMethod = source.readInt() != 0 ? source.readString8() : null; + mArg = source.readInt() != 0 ? source.readString8() : null; final int valuesSize = source.readInt(); if (valuesSize != -1) { mValues = new ArrayMap<>(valuesSize); @@ -107,7 +107,7 @@ public class ContentProviderOperation implements Parcelable { } else { mExtras = null; } - mSelection = source.readInt() != 0 ? source.readString() : null; + mSelection = source.readInt() != 0 ? source.readString8() : null; mSelectionArgs = source.readSparseArray(null); mExpectedCount = source.readInt() != 0 ? source.readInt() : null; mYieldAllowed = source.readInt() != 0; @@ -135,13 +135,13 @@ public class ContentProviderOperation implements Parcelable { Uri.writeToParcel(dest, mUri); if (mMethod != null) { dest.writeInt(1); - dest.writeString(mMethod); + dest.writeString8(mMethod); } else { dest.writeInt(0); } if (mArg != null) { dest.writeInt(1); - dest.writeString(mArg); + dest.writeString8(mArg); } else { dest.writeInt(0); } @@ -159,7 +159,7 @@ public class ContentProviderOperation implements Parcelable { } if (mSelection != null) { dest.writeInt(1); - dest.writeString(mSelection); + dest.writeString8(mSelection); } else { dest.writeInt(0); } @@ -591,7 +591,7 @@ public class ContentProviderOperation implements Parcelable { public BackReference(Parcel src) { this.fromIndex = src.readInt(); if (src.readInt() != 0) { - this.fromKey = src.readString(); + this.fromKey = src.readString8(); } else { this.fromKey = null; } @@ -620,7 +620,7 @@ public class ContentProviderOperation implements Parcelable { dest.writeInt(fromIndex); if (fromKey != null) { dest.writeInt(1); - dest.writeString(fromKey); + dest.writeString8(fromKey); } else { dest.writeInt(0); } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index b1d6c830d3b7..def150ab49e5 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -888,8 +888,8 @@ public class Intent implements Parcelable, Cloneable { public ShortcutIconResource createFromParcel(Parcel source) { ShortcutIconResource icon = new ShortcutIconResource(); - icon.packageName = source.readString(); - icon.resourceName = source.readString(); + icon.packageName = source.readString8(); + icon.resourceName = source.readString8(); return icon; } @@ -906,8 +906,8 @@ public class Intent implements Parcelable, Cloneable { } public void writeToParcel(Parcel dest, int flags) { - dest.writeString(packageName); - dest.writeString(resourceName); + dest.writeString8(packageName); + dest.writeString8(resourceName); } @Override @@ -10807,12 +10807,12 @@ public class Intent implements Parcelable, Cloneable { } public void writeToParcel(Parcel out, int flags) { - out.writeString(mAction); + out.writeString8(mAction); Uri.writeToParcel(out, mData); - out.writeString(mType); - out.writeString(mIdentifier); + out.writeString8(mType); + out.writeString8(mIdentifier); out.writeInt(mFlags); - out.writeString(mPackage); + out.writeString8(mPackage); ComponentName.writeToParcel(mComponent, out); if (mSourceBounds != null) { @@ -10826,7 +10826,7 @@ public class Intent implements Parcelable, Cloneable { final int N = mCategories.size(); out.writeInt(N); for (int i=0; i<N; i++) { - out.writeString(mCategories.valueAt(i)); + out.writeString8(mCategories.valueAt(i)); } } else { out.writeInt(0); @@ -10865,12 +10865,12 @@ public class Intent implements Parcelable, Cloneable { } public void readFromParcel(Parcel in) { - setAction(in.readString()); + setAction(in.readString8()); mData = Uri.CREATOR.createFromParcel(in); - mType = in.readString(); - mIdentifier = in.readString(); + mType = in.readString8(); + mIdentifier = in.readString8(); mFlags = in.readInt(); - mPackage = in.readString(); + mPackage = in.readString8(); mComponent = ComponentName.readFromParcel(in); if (in.readInt() != 0) { @@ -10882,7 +10882,7 @@ public class Intent implements Parcelable, Cloneable { mCategories = new ArraySet<String>(); int i; for (i=0; i<N; i++) { - mCategories.add(in.readString().intern()); + mCategories.add(in.readString8().intern()); } } else { mCategories = null; diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index f25ce76d9365..b1f88693d9c0 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1206,17 +1206,17 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { dest.writeInt(theme); dest.writeInt(launchMode); dest.writeInt(documentLaunchMode); - dest.writeString(permission); - dest.writeString(taskAffinity); - dest.writeString(targetActivity); - dest.writeString(launchToken); + dest.writeString8(permission); + dest.writeString8(taskAffinity); + dest.writeString8(targetActivity); + dest.writeString8(launchToken); dest.writeInt(flags); dest.writeInt(privateFlags); dest.writeInt(screenOrientation); dest.writeInt(configChanges); dest.writeInt(softInputMode); dest.writeInt(uiOptions); - dest.writeString(parentActivityName); + dest.writeString8(parentActivityName); dest.writeInt(persistableMode); dest.writeInt(maxRecents); dest.writeInt(lockTaskLaunchMode); @@ -1227,7 +1227,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { dest.writeInt(0); } dest.writeInt(resizeMode); - dest.writeString(requestedVrComponent); + dest.writeString8(requestedVrComponent); dest.writeInt(rotationAnimation); dest.writeInt(colorMode); dest.writeFloat(maxAspectRatio); @@ -1327,17 +1327,17 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { theme = source.readInt(); launchMode = source.readInt(); documentLaunchMode = source.readInt(); - permission = source.readString(); - taskAffinity = source.readString(); - targetActivity = source.readString(); - launchToken = source.readString(); + permission = source.readString8(); + taskAffinity = source.readString8(); + targetActivity = source.readString8(); + launchToken = source.readString8(); flags = source.readInt(); privateFlags = source.readInt(); screenOrientation = source.readInt(); configChanges = source.readInt(); softInputMode = source.readInt(); uiOptions = source.readInt(); - parentActivityName = source.readString(); + parentActivityName = source.readString8(); persistableMode = source.readInt(); maxRecents = source.readInt(); lockTaskLaunchMode = source.readInt(); @@ -1345,7 +1345,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { windowLayout = new WindowLayout(source); } resizeMode = source.readInt(); - requestedVrComponent = source.readString(); + requestedVrComponent = source.readString8(); rotationAnimation = source.readInt(); colorMode = source.readInt(); maxAspectRatio = source.readFloat(); @@ -1386,7 +1386,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { gravity = source.readInt(); minWidth = source.readInt(); minHeight = source.readInt(); - windowLayoutAffinity = source.readString(); + windowLayoutAffinity = source.readString8(); } /** @@ -1476,7 +1476,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { dest.writeInt(gravity); dest.writeInt(minWidth); dest.writeInt(minHeight); - dest.writeString(windowLayoutAffinity); + dest.writeString8(windowLayoutAffinity); } } } diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index b67060111785..b37521b1189b 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -1704,10 +1704,10 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { return; } super.writeToParcel(dest, parcelableFlags); - dest.writeString(taskAffinity); - dest.writeString(permission); - dest.writeString(processName); - dest.writeString(className); + dest.writeString8(taskAffinity); + dest.writeString8(permission); + dest.writeString8(processName); + dest.writeString8(className); dest.writeInt(theme); dest.writeInt(flags); dest.writeInt(privateFlags); @@ -1721,28 +1721,28 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } else { dest.writeInt(0); } - dest.writeString(scanSourceDir); - dest.writeString(scanPublicSourceDir); - dest.writeString(sourceDir); - dest.writeString(publicSourceDir); + dest.writeString8(scanSourceDir); + dest.writeString8(scanPublicSourceDir); + dest.writeString8(sourceDir); + dest.writeString8(publicSourceDir); dest.writeStringArray(splitNames); dest.writeStringArray(splitSourceDirs); dest.writeStringArray(splitPublicSourceDirs); dest.writeSparseArray((SparseArray) splitDependencies); - dest.writeString(nativeLibraryDir); - dest.writeString(secondaryNativeLibraryDir); - dest.writeString(nativeLibraryRootDir); + dest.writeString8(nativeLibraryDir); + dest.writeString8(secondaryNativeLibraryDir); + dest.writeString8(nativeLibraryRootDir); dest.writeInt(nativeLibraryRootRequiresIsa ? 1 : 0); - dest.writeString(primaryCpuAbi); - dest.writeString(secondaryCpuAbi); + dest.writeString8(primaryCpuAbi); + dest.writeString8(secondaryCpuAbi); dest.writeStringArray(resourceDirs); - dest.writeString(seInfo); - dest.writeString(seInfoUser); + dest.writeString8(seInfo); + dest.writeString8(seInfoUser); dest.writeStringArray(sharedLibraryFiles); dest.writeTypedList(sharedLibraryInfos); - dest.writeString(dataDir); - dest.writeString(deviceProtectedDataDir); - dest.writeString(credentialProtectedDataDir); + dest.writeString8(dataDir); + dest.writeString8(deviceProtectedDataDir); + dest.writeString8(credentialProtectedDataDir); dest.writeInt(uid); dest.writeInt(minSdkVersion); dest.writeInt(targetSdkVersion); @@ -1750,8 +1750,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(enabled ? 1 : 0); dest.writeInt(enabledSetting); dest.writeInt(installLocation); - dest.writeString(manageSpaceActivityName); - dest.writeString(backupAgentName); + dest.writeString8(manageSpaceActivityName); + dest.writeString8(backupAgentName); dest.writeInt(descriptionRes); dest.writeInt(uiOptions); dest.writeInt(fullBackupContent); @@ -1759,16 +1759,16 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(networkSecurityConfigRes); dest.writeInt(category); dest.writeInt(targetSandboxVersion); - dest.writeString(classLoaderName); + dest.writeString8(classLoaderName); dest.writeStringArray(splitClassLoaderNames); dest.writeInt(compileSdkVersion); - dest.writeString(compileSdkVersionCodename); - dest.writeString(appComponentFactory); + dest.writeString8(compileSdkVersionCodename); + dest.writeString8(appComponentFactory); dest.writeInt(iconRes); dest.writeInt(roundIconRes); dest.writeInt(mHiddenApiPolicy); dest.writeInt(hiddenUntilInstalled ? 1 : 0); - dest.writeString(zygotePreloadName); + dest.writeString8(zygotePreloadName); dest.writeInt(gwpAsanMode); } @@ -1788,10 +1788,10 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { @SuppressWarnings("unchecked") private ApplicationInfo(Parcel source) { super(source); - taskAffinity = source.readString(); - permission = source.readString(); - processName = source.readString(); - className = source.readString(); + taskAffinity = source.readString8(); + permission = source.readString8(); + processName = source.readString8(); + className = source.readString8(); theme = source.readInt(); flags = source.readInt(); privateFlags = source.readInt(); @@ -1802,28 +1802,28 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { storageUuid = new UUID(source.readLong(), source.readLong()); volumeUuid = StorageManager.convert(storageUuid); } - scanSourceDir = source.readString(); - scanPublicSourceDir = source.readString(); - sourceDir = source.readString(); - publicSourceDir = source.readString(); + scanSourceDir = source.readString8(); + scanPublicSourceDir = source.readString8(); + sourceDir = source.readString8(); + publicSourceDir = source.readString8(); splitNames = source.readStringArray(); splitSourceDirs = source.readStringArray(); splitPublicSourceDirs = source.readStringArray(); splitDependencies = source.readSparseArray(null); - nativeLibraryDir = source.readString(); - secondaryNativeLibraryDir = source.readString(); - nativeLibraryRootDir = source.readString(); + nativeLibraryDir = source.readString8(); + secondaryNativeLibraryDir = source.readString8(); + nativeLibraryRootDir = source.readString8(); nativeLibraryRootRequiresIsa = source.readInt() != 0; - primaryCpuAbi = source.readString(); - secondaryCpuAbi = source.readString(); + primaryCpuAbi = source.readString8(); + secondaryCpuAbi = source.readString8(); resourceDirs = source.readStringArray(); - seInfo = source.readString(); - seInfoUser = source.readString(); + seInfo = source.readString8(); + seInfoUser = source.readString8(); sharedLibraryFiles = source.readStringArray(); sharedLibraryInfos = source.createTypedArrayList(SharedLibraryInfo.CREATOR); - dataDir = source.readString(); - deviceProtectedDataDir = source.readString(); - credentialProtectedDataDir = source.readString(); + dataDir = source.readString8(); + deviceProtectedDataDir = source.readString8(); + credentialProtectedDataDir = source.readString8(); uid = source.readInt(); minSdkVersion = source.readInt(); targetSdkVersion = source.readInt(); @@ -1831,8 +1831,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { enabled = source.readInt() != 0; enabledSetting = source.readInt(); installLocation = source.readInt(); - manageSpaceActivityName = source.readString(); - backupAgentName = source.readString(); + manageSpaceActivityName = source.readString8(); + backupAgentName = source.readString8(); descriptionRes = source.readInt(); uiOptions = source.readInt(); fullBackupContent = source.readInt(); @@ -1840,16 +1840,16 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { networkSecurityConfigRes = source.readInt(); category = source.readInt(); targetSandboxVersion = source.readInt(); - classLoaderName = source.readString(); + classLoaderName = source.readString8(); splitClassLoaderNames = source.readStringArray(); compileSdkVersion = source.readInt(); - compileSdkVersionCodename = source.readString(); - appComponentFactory = source.readString(); + compileSdkVersionCodename = source.readString8(); + appComponentFactory = source.readString8(); iconRes = source.readInt(); roundIconRes = source.readInt(); mHiddenApiPolicy = source.readInt(); hiddenUntilInstalled = source.readInt() != 0; - zygotePreloadName = source.readString(); + zygotePreloadName = source.readString8(); gwpAsanMode = source.readInt(); } diff --git a/core/java/android/content/pm/ComponentInfo.java b/core/java/android/content/pm/ComponentInfo.java index 362098c447ce..628bcd70cdf6 100644 --- a/core/java/android/content/pm/ComponentInfo.java +++ b/core/java/android/content/pm/ComponentInfo.java @@ -197,8 +197,8 @@ public class ComponentInfo extends PackageItemInfo { public void writeToParcel(Parcel dest, int parcelableFlags) { super.writeToParcel(dest, parcelableFlags); applicationInfo.writeToParcel(dest, parcelableFlags); - dest.writeString(processName); - dest.writeString(splitName); + dest.writeString8(processName); + dest.writeString8(splitName); dest.writeInt(descriptionRes); dest.writeInt(enabled ? 1 : 0); dest.writeInt(exported ? 1 : 0); @@ -208,8 +208,8 @@ public class ComponentInfo extends PackageItemInfo { protected ComponentInfo(Parcel source) { super(source); applicationInfo = ApplicationInfo.CREATOR.createFromParcel(source); - processName = source.readString(); - splitName = source.readString(); + processName = source.readString8(); + splitName = source.readString8(); descriptionRes = source.readInt(); enabled = (source.readInt() != 0); exported = (source.readInt() != 0); diff --git a/core/java/android/content/pm/FeatureInfo.java b/core/java/android/content/pm/FeatureInfo.java index 9f3ab77062ef..89269e0d7aa5 100644 --- a/core/java/android/content/pm/FeatureInfo.java +++ b/core/java/android/content/pm/FeatureInfo.java @@ -108,7 +108,7 @@ public class FeatureInfo implements Parcelable { @Override public void writeToParcel(Parcel dest, int parcelableFlags) { - dest.writeString(name); + dest.writeString8(name); dest.writeInt(version); dest.writeInt(reqGlEsVersion); dest.writeInt(flags); @@ -138,7 +138,7 @@ public class FeatureInfo implements Parcelable { }; private FeatureInfo(Parcel source) { - name = source.readString(); + name = source.readString8(); version = source.readInt(); reqGlEsVersion = source.readInt(); flags = source.readInt(); diff --git a/core/java/android/content/pm/IDataLoaderStatusListener.aidl b/core/java/android/content/pm/IDataLoaderStatusListener.aidl index 9819b5d4eeb9..ffe8b183e926 100644 --- a/core/java/android/content/pm/IDataLoaderStatusListener.aidl +++ b/core/java/android/content/pm/IDataLoaderStatusListener.aidl @@ -21,16 +21,29 @@ package android.content.pm; * @hide */ oneway interface IDataLoaderStatusListener { - /** Data loader status */ + /** When this status is returned from DataLoader, it means that the DataLoader + * process is running, bound to and has handled onCreate(). */ const int DATA_LOADER_CREATED = 0; + /** Listener will receive this status when the DataLoader process died, + * binder disconnected or class destroyed. */ const int DATA_LOADER_DESTROYED = 1; + /** DataLoader can receive missing pages and read pages notifications, + * and ready to provide data. */ const int DATA_LOADER_STARTED = 2; + /** DataLoader no longer ready to provide data and is not receiving + * any notifications from IncFS. */ const int DATA_LOADER_STOPPED = 3; + /** DataLoader streamed everything necessary to continue installation. */ const int DATA_LOADER_IMAGE_READY = 4; + /** Installation can't continue as DataLoader failed to stream necessary data. */ const int DATA_LOADER_IMAGE_NOT_READY = 5; + /** DataLoader reports that this instance is invalid and can never be restored. + * Warning: this is a terminal status that data loader should use carefully and + * the system should almost never use - e.g. only if all recovery attempts + * fail and all retry limits are exceeded. */ const int DATA_LOADER_UNRECOVERABLE = 6; /** Data loader status callback */ diff --git a/core/java/android/content/pm/InstrumentationInfo.java b/core/java/android/content/pm/InstrumentationInfo.java index 574a1ee2d0ce..745a6c1a0dff 100644 --- a/core/java/android/content/pm/InstrumentationInfo.java +++ b/core/java/android/content/pm/InstrumentationInfo.java @@ -157,21 +157,21 @@ public class InstrumentationInfo extends PackageItemInfo implements Parcelable { public void writeToParcel(Parcel dest, int parcelableFlags) { super.writeToParcel(dest, parcelableFlags); - dest.writeString(targetPackage); - dest.writeString(targetProcesses); - dest.writeString(sourceDir); - dest.writeString(publicSourceDir); + dest.writeString8(targetPackage); + dest.writeString8(targetProcesses); + dest.writeString8(sourceDir); + dest.writeString8(publicSourceDir); dest.writeStringArray(splitNames); dest.writeStringArray(splitSourceDirs); dest.writeStringArray(splitPublicSourceDirs); dest.writeSparseArray((SparseArray) splitDependencies); - dest.writeString(dataDir); - dest.writeString(deviceProtectedDataDir); - dest.writeString(credentialProtectedDataDir); - dest.writeString(primaryCpuAbi); - dest.writeString(secondaryCpuAbi); - dest.writeString(nativeLibraryDir); - dest.writeString(secondaryNativeLibraryDir); + dest.writeString8(dataDir); + dest.writeString8(deviceProtectedDataDir); + dest.writeString8(credentialProtectedDataDir); + dest.writeString8(primaryCpuAbi); + dest.writeString8(secondaryCpuAbi); + dest.writeString8(nativeLibraryDir); + dest.writeString8(secondaryNativeLibraryDir); dest.writeInt((handleProfiling == false) ? 0 : 1); dest.writeInt((functionalTest == false) ? 0 : 1); } @@ -189,21 +189,21 @@ public class InstrumentationInfo extends PackageItemInfo implements Parcelable { @SuppressWarnings("unchecked") private InstrumentationInfo(Parcel source) { super(source); - targetPackage = source.readString(); - targetProcesses = source.readString(); - sourceDir = source.readString(); - publicSourceDir = source.readString(); + targetPackage = source.readString8(); + targetProcesses = source.readString8(); + sourceDir = source.readString8(); + publicSourceDir = source.readString8(); splitNames = source.readStringArray(); splitSourceDirs = source.readStringArray(); splitPublicSourceDirs = source.readStringArray(); splitDependencies = source.readSparseArray(null); - dataDir = source.readString(); - deviceProtectedDataDir = source.readString(); - credentialProtectedDataDir = source.readString(); - primaryCpuAbi = source.readString(); - secondaryCpuAbi = source.readString(); - nativeLibraryDir = source.readString(); - secondaryNativeLibraryDir = source.readString(); + dataDir = source.readString8(); + deviceProtectedDataDir = source.readString8(); + credentialProtectedDataDir = source.readString8(); + primaryCpuAbi = source.readString8(); + secondaryCpuAbi = source.readString8(); + nativeLibraryDir = source.readString8(); + secondaryNativeLibraryDir = source.readString8(); handleProfiling = source.readInt() != 0; functionalTest = source.readInt() != 0; } diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index 85c698f3fb0c..bb56ef7fd3a0 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -441,14 +441,14 @@ public class PackageInfo implements Parcelable { public void writeToParcel(Parcel dest, int parcelableFlags) { // Allow ApplicationInfo to be squashed. final boolean prevAllowSquashing = dest.allowSquashing(); - dest.writeString(packageName); + dest.writeString8(packageName); dest.writeStringArray(splitNames); dest.writeInt(versionCode); dest.writeInt(versionCodeMajor); - dest.writeString(versionName); + dest.writeString8(versionName); dest.writeInt(baseRevisionCode); dest.writeIntArray(splitRevisionCodes); - dest.writeString(sharedUserId); + dest.writeString8(sharedUserId); dest.writeInt(sharedUserLabel); if (applicationInfo != null) { dest.writeInt(1); @@ -475,14 +475,14 @@ public class PackageInfo implements Parcelable { dest.writeInt(isStub ? 1 : 0); dest.writeInt(coreApp ? 1 : 0); dest.writeInt(requiredForAllUsers ? 1 : 0); - dest.writeString(restrictedAccountType); - dest.writeString(requiredAccountType); - dest.writeString(overlayTarget); - dest.writeString(overlayCategory); + dest.writeString8(restrictedAccountType); + dest.writeString8(requiredAccountType); + dest.writeString8(overlayTarget); + dest.writeString8(overlayCategory); dest.writeInt(overlayPriority); dest.writeBoolean(mOverlayIsStatic); dest.writeInt(compileSdkVersion); - dest.writeString(compileSdkVersionCodename); + dest.writeString8(compileSdkVersionCodename); if (signingInfo != null) { dest.writeInt(1); signingInfo.writeToParcel(dest, parcelableFlags); @@ -508,14 +508,14 @@ public class PackageInfo implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) private PackageInfo(Parcel source) { - packageName = source.readString(); + packageName = source.readString8(); splitNames = source.createStringArray(); versionCode = source.readInt(); versionCodeMajor = source.readInt(); - versionName = source.readString(); + versionName = source.readString8(); baseRevisionCode = source.readInt(); splitRevisionCodes = source.createIntArray(); - sharedUserId = source.readString(); + sharedUserId = source.readString8(); sharedUserLabel = source.readInt(); int hasApp = source.readInt(); if (hasApp != 0) { @@ -540,14 +540,14 @@ public class PackageInfo implements Parcelable { isStub = source.readInt() != 0; coreApp = source.readInt() != 0; requiredForAllUsers = source.readInt() != 0; - restrictedAccountType = source.readString(); - requiredAccountType = source.readString(); - overlayTarget = source.readString(); - overlayCategory = source.readString(); + restrictedAccountType = source.readString8(); + requiredAccountType = source.readString8(); + overlayTarget = source.readString8(); + overlayCategory = source.readString8(); overlayPriority = source.readInt(); mOverlayIsStatic = source.readBoolean(); compileSdkVersion = source.readInt(); - compileSdkVersionCodename = source.readString(); + compileSdkVersionCodename = source.readString8(); int hasSigningInfo = source.readInt(); if (hasSigningInfo != 0) { signingInfo = SigningInfo.CREATOR.createFromParcel(source); diff --git a/core/java/android/content/pm/PackageItemInfo.java b/core/java/android/content/pm/PackageItemInfo.java index 7fd5531bf20d..d41ace5bcf62 100644 --- a/core/java/android/content/pm/PackageItemInfo.java +++ b/core/java/android/content/pm/PackageItemInfo.java @@ -422,8 +422,8 @@ public class PackageItemInfo { } public void writeToParcel(Parcel dest, int parcelableFlags) { - dest.writeString(name); - dest.writeString(packageName); + dest.writeString8(name); + dest.writeString8(packageName); dest.writeInt(labelRes); TextUtils.writeToParcel(nonLocalizedLabel, dest, parcelableFlags); dest.writeInt(icon); @@ -452,8 +452,8 @@ public class PackageItemInfo { } protected PackageItemInfo(Parcel source) { - name = source.readString(); - packageName = source.readString(); + name = source.readString8(); + packageName = source.readString8(); labelRes = source.readInt(); nonLocalizedLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 1dadbda1918b..85bafd9d37e2 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -1695,7 +1695,7 @@ public class PackageParser { } // Check to see if overlay should be excluded based on system property condition - if (!checkRequiredSystemProperty(requiredSystemPropertyName, + if (!checkRequiredSystemProperties(requiredSystemPropertyName, requiredSystemPropertyValue)) { Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and " + codePath + ": overlay ignored due to required system property: " @@ -1997,7 +1997,7 @@ public class PackageParser { } // check to see if overlay should be excluded based on system property condition - if (!checkRequiredSystemProperty(propName, propValue)) { + if (!checkRequiredSystemProperties(propName, propValue)) { Slog.i(TAG, "Skipping target and overlay pair " + pkg.mOverlayTarget + " and " + pkg.baseCodePath+ ": overlay ignored due to required system property: " + propName + " with value: " + propValue); @@ -2427,24 +2427,42 @@ public class PackageParser { /** * Returns {@code true} if both the property name and value are empty or if the given system - * property is set to the specified value. In all other cases, returns {@code false} + * property is set to the specified value. Properties can be one or more, and if properties are + * more than one, they must be separated by comma, and count of names and values must be equal, + * and also every given system property must be set to the corresponding value. + * In all other cases, returns {@code false} */ - public static boolean checkRequiredSystemProperty(String propName, String propValue) { - if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) { - if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) { + public static boolean checkRequiredSystemProperties(@Nullable String rawPropNames, + @Nullable String rawPropValues) { + if (TextUtils.isEmpty(rawPropNames) || TextUtils.isEmpty(rawPropValues)) { + if (!TextUtils.isEmpty(rawPropNames) || !TextUtils.isEmpty(rawPropValues)) { // malformed condition - incomplete - Slog.w(TAG, "Disabling overlay - incomplete property :'" + propName - + "=" + propValue + "' - require both requiredSystemPropertyName" - + " AND requiredSystemPropertyValue to be specified."); + Slog.w(TAG, "Disabling overlay - incomplete property :'" + rawPropNames + + "=" + rawPropValues + "' - require both requiredSystemPropertyName" + + " AND requiredSystemPropertyValue to be specified."); return false; } // no valid condition set - so no exclusion criteria, overlay will be included. return true; } - // check property value - make sure it is both set and equal to expected value - final String currValue = SystemProperties.get(propName); - return (currValue != null && currValue.equals(propValue)); + final String[] propNames = rawPropNames.split(","); + final String[] propValues = rawPropValues.split(","); + + if (propNames.length != propValues.length) { + Slog.w(TAG, "Disabling overlay - property :'" + rawPropNames + + "=" + rawPropValues + "' - require both requiredSystemPropertyName" + + " AND requiredSystemPropertyValue lists to have the same size."); + return false; + } + for (int i = 0; i < propNames.length; i++) { + // Check property value: make sure it is both set and equal to expected value + final String currValue = SystemProperties.get(propNames[i]); + if (!TextUtils.equals(currValue, propValues[i])) { + return false; + } + } + return true; } /** diff --git a/core/java/android/content/pm/PackageParserCacheHelper.java b/core/java/android/content/pm/PackageParserCacheHelper.java index 44def3321c34..8212224e114c 100644 --- a/core/java/android/content/pm/PackageParserCacheHelper.java +++ b/core/java/android/content/pm/PackageParserCacheHelper.java @@ -78,10 +78,19 @@ public class PackageParserCacheHelper { /** * Read an string index from a parcel, and returns the corresponding string from the pool. */ - @Override public String readString(Parcel p) { return mStrings.get(p.readInt()); } + + @Override + public String readString8(Parcel p) { + return readString(p); + } + + @Override + public String readString16(Parcel p) { + return readString(p); + } } /** @@ -110,7 +119,6 @@ public class PackageParserCacheHelper { * Instead of writing a string directly to a parcel, this method adds it to the pool, * and write the index in the pool to the parcel. */ - @Override public void writeString(Parcel p, String s) { final Integer cur = mIndexes.get(s); if (cur != null) { @@ -133,6 +141,16 @@ public class PackageParserCacheHelper { } } + @Override + public void writeString8(Parcel p, String s) { + writeString(p, s); + } + + @Override + public void writeString16(Parcel p, String s) { + writeString(p, s); + } + /** * Closes a parcel by appending the string pool at the end and updating the pool offset, * which it assumes is at the first byte. It also uninstalls itself as a read-write helper. diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java index 07d42dc823c4..3984ade73d6c 100644 --- a/core/java/android/content/pm/ProviderInfo.java +++ b/core/java/android/content/pm/ProviderInfo.java @@ -145,9 +145,9 @@ public final class ProviderInfo extends ComponentInfo @Override public void writeToParcel(Parcel out, int parcelableFlags) { super.writeToParcel(out, parcelableFlags); - out.writeString(authority); - out.writeString(readPermission); - out.writeString(writePermission); + out.writeString8(authority); + out.writeString8(readPermission); + out.writeString8(writePermission); out.writeInt(grantUriPermissions ? 1 : 0); out.writeInt(forceUriPermissions ? 1 : 0); out.writeTypedArray(uriPermissionPatterns, parcelableFlags); @@ -175,9 +175,9 @@ public final class ProviderInfo extends ComponentInfo private ProviderInfo(Parcel in) { super(in); - authority = in.readString(); - readPermission = in.readString(); - writePermission = in.readString(); + authority = in.readString8(); + readPermission = in.readString8(); + writePermission = in.readString8(); grantUriPermissions = in.readInt() != 0; forceUriPermissions = in.readInt() != 0; uriPermissionPatterns = in.createTypedArray(PatternMatcher.CREATOR); diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 5f90b6c43c60..d3f9e2486a64 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -244,7 +244,7 @@ public class ServiceInfo extends ComponentInfo public void writeToParcel(Parcel dest, int parcelableFlags) { super.writeToParcel(dest, parcelableFlags); - dest.writeString(permission); + dest.writeString8(permission); dest.writeInt(flags); dest.writeInt(mForegroundServiceType); } @@ -261,7 +261,7 @@ public class ServiceInfo extends ComponentInfo private ServiceInfo(Parcel source) { super(source); - permission = source.readString(); + permission = source.readString8(); flags = source.readInt(); mForegroundServiceType = source.readInt(); } diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 27399e4b39bc..2f416a2538ba 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -413,7 +413,7 @@ public class ApkLiteParseUtils { } // Check to see if overlay should be excluded based on system property condition - if (!PackageParser.checkRequiredSystemProperty(requiredSystemPropertyName, + if (!PackageParser.checkRequiredSystemProperties(requiredSystemPropertyName, requiredSystemPropertyValue)) { Slog.i(TAG, "Skipping target and overlay pair " + targetPackage + " and " + codePath + ": overlay ignored due to required system property: " diff --git a/core/java/android/content/pm/parsing/ParsingPackageUtils.java b/core/java/android/content/pm/parsing/ParsingPackageUtils.java index 29ece4924e5c..88f4c31b82cc 100644 --- a/core/java/android/content/pm/parsing/ParsingPackageUtils.java +++ b/core/java/android/content/pm/parsing/ParsingPackageUtils.java @@ -84,7 +84,6 @@ import android.os.Build; import android.os.Bundle; import android.os.FileUtils; import android.os.RemoteException; -import android.os.SystemProperties; import android.os.Trace; import android.os.ext.SdkExtensions; import android.text.TextUtils; @@ -2348,7 +2347,7 @@ public class ParsingPackageUtils { R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyName); String propValue = sa.getString( R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyValue); - if (!checkOverlayRequiredSystemProperty(propName, propValue)) { + if (!PackageParser.checkRequiredSystemProperties(propName, propValue)) { Slog.i(TAG, "Skipping target and overlay pair " + target + " and " + pkg.getBaseCodePath() + ": overlay ignored due to required system property: " @@ -2522,24 +2521,6 @@ public class ParsingPackageUtils { } } - private static boolean checkOverlayRequiredSystemProperty(String propName, String propValue) { - if (TextUtils.isEmpty(propName) || TextUtils.isEmpty(propValue)) { - if (!TextUtils.isEmpty(propName) || !TextUtils.isEmpty(propValue)) { - // malformed condition - incomplete - Slog.w(TAG, "Disabling overlay - incomplete property :'" + propName - + "=" + propValue + "' - require both requiredSystemPropertyName" - + " AND requiredSystemPropertyValue to be specified."); - return false; - } - // no valid condition set - so no exclusion criteria, overlay will be included. - return true; - } - - // check property value - make sure it is both set and equal to expected value - final String currValue = SystemProperties.get(propName); - return (currValue != null && currValue.equals(propValue)); - } - /** * This is a pre-density application which will get scaled - instead of being pixel perfect. * This type of application is not resizable. diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index f7c96a3a02c1..2f67f6ddc082 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -228,19 +228,26 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } catch (SQLiteCantOpenDatabaseException e) { String message = String.format("Cannot open database '%s'", file); - final Path path = FileSystems.getDefault().getPath(file); - final Path dir = path.getParent(); - - if (!Files.isDirectory(dir)) { - message += ": Directory " + dir + " doesn't exist"; - } else if (!Files.exists(path)) { - message += ": File " + path + " doesn't exist"; - } else if (!Files.isReadable(path)) { - message += ": File " + path + " is not readable"; - } else if (Files.isDirectory(path)) { - message += ": Path " + path + " is a directory"; - } else { - message += ": Unknown reason"; + try { + // Try to diagnose for common reasons. If something fails in here, that's fine; + // just swallow the exception. + + final Path path = FileSystems.getDefault().getPath(file); + final Path dir = path.getParent(); + + if (!Files.isDirectory(dir)) { + message += ": Directory " + dir + " doesn't exist"; + } else if (!Files.exists(path)) { + message += ": File " + path + " doesn't exist"; + } else if (!Files.isReadable(path)) { + message += ": File " + path + " is not readable"; + } else if (Files.isDirectory(path)) { + message += ": Path " + path + " is a directory"; + } else { + message += ": Unknown reason"; + } + } catch (Throwable th) { + message += ": Unknown reason; cannot examine filesystem: " + th.getMessage(); } throw new SQLiteCantOpenDatabaseException(message, e); } finally { diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 462110f5a795..ad9bf0745779 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -65,21 +65,23 @@ public abstract class DisplayManagerInternal { public abstract boolean isProximitySensorAvailable(); /** - * Take a screenshot of the specified display and return a buffer. + * Screenshot for internal system-only use such as rotation, etc. This method includes + * secure layers and the result should never be exposed to non-system applications. + * This method does not apply any rotation and provides the output in natural orientation. * * @param displayId The display id to take the screenshot of. * @return The buffer or null if we have failed. */ - public abstract SurfaceControl.ScreenshotGraphicBuffer screenshot(int displayId); + public abstract SurfaceControl.ScreenshotGraphicBuffer systemScreenshot(int displayId); /** - * Take a screenshot without secure layer of the specified display and return a buffer. + * General screenshot functionality that excludes secure layers and applies appropriate + * rotation that the device is currently in. * * @param displayId The display id to take the screenshot of. * @return The buffer or null if we have failed. */ - public abstract SurfaceControl.ScreenshotGraphicBuffer screenshotWithoutSecureLayer( - int displayId); + public abstract SurfaceControl.ScreenshotGraphicBuffer userScreenshot(int displayId); /** * Returns information about the specified logical display. diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 525bbfdbb06d..1cb4fe8cf4e7 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -500,7 +500,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { } static Uri readFrom(Parcel parcel) { - return new StringUri(parcel.readString()); + return new StringUri(parcel.readString8()); } public int describeContents() { @@ -509,7 +509,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(TYPE_ID); - parcel.writeString(uriString); + parcel.writeString8(uriString); } /** Cached scheme separator index. */ @@ -875,7 +875,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { static Uri readFrom(Parcel parcel) { return new OpaqueUri( - parcel.readString(), + parcel.readString8(), Part.readFrom(parcel), Part.readFrom(parcel) ); @@ -887,7 +887,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(TYPE_ID); - parcel.writeString(scheme); + parcel.writeString8(scheme); ssp.writeTo(parcel); fragment.writeTo(parcel); } @@ -1195,7 +1195,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { static Uri readFrom(Parcel parcel) { return new HierarchicalUri( - parcel.readString(), + parcel.readString8(), Part.readFrom(parcel), PathPart.readFrom(parcel), Part.readFrom(parcel), @@ -1209,7 +1209,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(TYPE_ID); - parcel.writeString(scheme); + parcel.writeString8(scheme); authority.writeTo(parcel); path.writeTo(parcel); query.writeTo(parcel); @@ -2028,7 +2028,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { + mCanonicalRepresentation + ")"); } parcel.writeInt(mCanonicalRepresentation); - parcel.writeString(canonicalValue); + parcel.writeString8(canonicalValue); } } @@ -2060,7 +2060,7 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { static Part readFrom(Parcel parcel) { int representation = parcel.readInt(); - String value = parcel.readString(); + String value = parcel.readString8(); switch (representation) { case REPRESENTATION_ENCODED: return fromEncoded(value); @@ -2251,9 +2251,9 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { int representation = parcel.readInt(); switch (representation) { case REPRESENTATION_ENCODED: - return fromEncoded(parcel.readString()); + return fromEncoded(parcel.readString8()); case REPRESENTATION_DECODED: - return fromDecoded(parcel.readString()); + return fromDecoded(parcel.readString8()); default: throw new IllegalArgumentException("Unknown representation: " + representation); } diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index f0b7b5fa5a1a..93f6607ff9d4 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -307,7 +307,9 @@ public final class Parcel { @FastNative private static native void nativeWriteDouble(long nativePtr, double val); @FastNative - static native void nativeWriteString(long nativePtr, String val); + private static native void nativeWriteString8(long nativePtr, String val); + @FastNative + private static native void nativeWriteString16(long nativePtr, String val); @FastNative private static native void nativeWriteStrongBinder(long nativePtr, IBinder val); @FastNative @@ -325,7 +327,9 @@ public final class Parcel { @CriticalNative private static native double nativeReadDouble(long nativePtr); @FastNative - static native String nativeReadString(long nativePtr); + private static native String nativeReadString8(long nativePtr); + @FastNative + private static native String nativeReadString16(long nativePtr); @FastNative private static native IBinder nativeReadStrongBinder(long nativePtr); @FastNative @@ -386,8 +390,12 @@ public final class Parcel { * must use {@link #writeStringNoHelper(String)} to avoid * infinity recursive calls. */ - public void writeString(Parcel p, String s) { - nativeWriteString(p.mNativePtr, s); + public void writeString8(Parcel p, String s) { + p.writeString8NoHelper(s); + } + + public void writeString16(Parcel p, String s) { + p.writeString16NoHelper(s); } /** @@ -395,8 +403,12 @@ public final class Parcel { * must use {@link #readStringNoHelper()} to avoid * infinity recursive calls. */ - public String readString(Parcel p) { - return nativeReadString(p.mNativePtr); + public String readString8(Parcel p) { + return p.readString8NoHelper(); + } + + public String readString16(Parcel p) { + return p.readString16NoHelper(); } } @@ -759,7 +771,17 @@ public final class Parcel { * growing dataCapacity() if needed. */ public final void writeString(@Nullable String val) { - mReadWriteHelper.writeString(this, val); + writeString16(val); + } + + /** {@hide} */ + public final void writeString8(@Nullable String val) { + mReadWriteHelper.writeString8(this, val); + } + + /** {@hide} */ + public final void writeString16(@Nullable String val) { + mReadWriteHelper.writeString16(this, val); } /** @@ -770,7 +792,17 @@ public final class Parcel { * @hide */ public void writeStringNoHelper(@Nullable String val) { - nativeWriteString(mNativePtr, val); + writeString16NoHelper(val); + } + + /** {@hide} */ + public void writeString8NoHelper(@Nullable String val) { + nativeWriteString8(mNativePtr, val); + } + + /** {@hide} */ + public void writeString16NoHelper(@Nullable String val) { + nativeWriteString16(mNativePtr, val); } /** @@ -2337,7 +2369,17 @@ public final class Parcel { */ @Nullable public final String readString() { - return mReadWriteHelper.readString(this); + return readString16(); + } + + /** {@hide} */ + public final @Nullable String readString8() { + return mReadWriteHelper.readString8(this); + } + + /** {@hide} */ + public final @Nullable String readString16() { + return mReadWriteHelper.readString16(this); } /** @@ -2347,9 +2389,18 @@ public final class Parcel { * * @hide */ - @Nullable - public String readStringNoHelper() { - return nativeReadString(mNativePtr); + public @Nullable String readStringNoHelper() { + return readString16NoHelper(); + } + + /** {@hide} */ + public @Nullable String readString8NoHelper() { + return nativeReadString8(mNativePtr); + } + + /** {@hide} */ + public @Nullable String readString16NoHelper() { + return nativeReadString16(mNativePtr); } /** diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index ca861577ab37..1fef07156310 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -897,6 +897,34 @@ public abstract class VibrationEffect implements Parcelable { return -1; } + /** + * Scale all primitives of this effect. + * + * @param gamma the gamma adjustment to apply + * @param maxAmplitude the new maximum amplitude of the effect, must be between 0 and + * MAX_AMPLITUDE + * @throws IllegalArgumentException if maxAmplitude less than 0 or more than MAX_AMPLITUDE + * + * @return A {@link Composed} effect with same but scaled primitives. + */ + public Composed scale(float gamma, int maxAmplitude) { + if (maxAmplitude > MAX_AMPLITUDE || maxAmplitude < 0) { + throw new IllegalArgumentException( + "Amplitude is negative or greater than MAX_AMPLITUDE"); + } + if (gamma == 1.0f && maxAmplitude == MAX_AMPLITUDE) { + // Just return a copy of the original if there's no scaling to be done. + return new Composed(mPrimitiveEffects); + } + List<Composition.PrimitiveEffect> scaledPrimitives = new ArrayList<>(); + for (Composition.PrimitiveEffect primitive : mPrimitiveEffects) { + float adjustedScale = MathUtils.pow(primitive.scale, gamma); + float newScale = adjustedScale * maxAmplitude / (float) MAX_AMPLITUDE; + scaledPrimitives.add(new Composition.PrimitiveEffect( + primitive.id, newScale, primitive.delay)); + } + return new Composed(scaledPrimitives); + } /** * @hide diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index aee32ed769ac..a1a11ed54609 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -2447,7 +2447,12 @@ public class StorageManager { final String filePath = path.getCanonicalPath(); final StorageVolume volume = getStorageVolume(path); if (volume == null) { - throw new IllegalStateException("Failed to update quota type for " + filePath); + Log.w(TAG, "Failed to update quota type for " + filePath); + return; + } + if (!volume.isEmulated()) { + // We only support quota tracking on emulated filesystems + return; } final int userId = volume.getOwner().getIdentifier(); diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index af915966e7eb..1f6555c85a66 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -102,8 +102,8 @@ public class ZenModeConfig implements Parcelable { private static final boolean DEFAULT_ALLOW_REMINDERS = false; private static final boolean DEFAULT_ALLOW_EVENTS = false; private static final boolean DEFAULT_ALLOW_REPEAT_CALLERS = true; - private static final boolean DEFAULT_ALLOW_CONV = true; - private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; + private static final boolean DEFAULT_ALLOW_CONV = false; + private static final int DEFAULT_ALLOW_CONV_FROM = ZenPolicy.CONVERSATION_SENDERS_NONE; private static final boolean DEFAULT_CHANNELS_BYPASSING_DND = false; private static final int DEFAULT_SUPPRESSED_VISUAL_EFFECTS = 0; @@ -125,8 +125,8 @@ public class ZenModeConfig implements Parcelable { private static final String ALLOW_ATT_EVENTS = "events"; private static final String ALLOW_ATT_SCREEN_OFF = "visualScreenOff"; private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn"; - private static final String ALLOW_ATT_CONV = "conv"; - private static final String ALLOW_ATT_CONV_FROM = "convFrom"; + private static final String ALLOW_ATT_CONV = "convos"; + private static final String ALLOW_ATT_CONV_FROM = "convosFrom"; private static final String DISALLOW_TAG = "disallow"; private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects"; private static final String STATE_TAG = "state"; diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index df4ead13d5cb..28639b3ea2f7 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -748,7 +748,7 @@ public class TextUtils { int parcelableFlags) { if (cs instanceof Spanned) { p.writeInt(0); - p.writeString(cs.toString()); + p.writeString8(cs.toString()); Spanned sp = (Spanned) cs; Object[] os = sp.getSpans(0, cs.length(), Object.class); @@ -785,9 +785,9 @@ public class TextUtils { } else { p.writeInt(1); if (cs != null) { - p.writeString(cs.toString()); + p.writeString8(cs.toString()); } else { - p.writeString(null); + p.writeString8(null); } } } @@ -807,7 +807,7 @@ public class TextUtils { public CharSequence createFromParcel(Parcel p) { int kind = p.readInt(); - String string = p.readString(); + String string = p.readString8(); if (string == null) { return null; } diff --git a/core/java/android/view/IScrollCaptureClient.aidl b/core/java/android/view/IScrollCaptureClient.aidl new file mode 100644 index 000000000000..5f135a37dfef --- /dev/null +++ b/core/java/android/view/IScrollCaptureClient.aidl @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Rect; +import android.view.Surface; + + + /** + * Interface implemented by a client of the Scroll Capture framework to receive requests + * to start, capture images and end the session. + * + * {@hide} + */ +interface IScrollCaptureClient { + + /** + * Informs the client that it has been selected for scroll capture and should prepare to + * to begin handling capture requests. + */ + oneway void startCapture(in Surface surface); + + /** + * Request the client capture an image within the provided rectangle. + * + * @see android.view.ScrollCaptureCallback#onScrollCaptureRequest + */ + oneway void requestImage(in Rect captureArea); + + /** + * Inform the client that capture has ended. The client should shut down and release all + * local resources in use and prepare for return to normal interactive usage. + */ + oneway void endCapture(); +} diff --git a/core/java/android/view/IScrollCaptureController.aidl b/core/java/android/view/IScrollCaptureController.aidl new file mode 100644 index 000000000000..8474a00b302f --- /dev/null +++ b/core/java/android/view/IScrollCaptureController.aidl @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Point; +import android.graphics.Rect; +import android.view.Surface; + +import android.view.IScrollCaptureClient; + +/** + * Interface to a controller passed to the {@link IScrollCaptureClient} which provides the client an + * asynchronous callback channel for responses. + * + * {@hide} + */ +interface IScrollCaptureController { + /** + * Scroll capture is available, and a client connect has been returned. + * + * @param client interface to a ScrollCaptureCallback in the window process + * @param scrollAreaInWindow the location of scrolling in global (window) coordinate space + */ + oneway void onClientConnected(in IScrollCaptureClient client, in Rect scrollBounds, + in Point positionInWindow); + + /** + * Nothing in the window can be scrolled, scroll capture not offered. + */ + oneway void onClientUnavailable(); + + /** + * Notifies the system that the client has confirmed the request and is ready to begin providing + * image requests. + */ + oneway void onCaptureStarted(); + + /** + * Received a response from a capture request. + */ + oneway void onCaptureBufferSent(long frameNumber, in Rect capturedArea); + + /** + * Signals that the capture session has completed and the target window may be returned to + * normal interactive use. This may be due to normal shutdown, or after a timeout or other + * unrecoverable state change such as activity lifecycle, window visibility or focus. + */ + oneway void onConnectionClosed(); +} diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index 9f2d36de61cd..e09bf9d2e80a 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -21,15 +21,16 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.os.ParcelFileDescriptor; +import android.util.MergedConfiguration; +import android.view.DisplayCutout; import android.view.DragEvent; +import android.view.InsetsSourceControl; +import android.view.InsetsState; +import android.view.IScrollCaptureController; import android.view.KeyEvent; import android.view.MotionEvent; -import android.view.DisplayCutout; -import android.view.InsetsState; -import android.view.InsetsSourceControl; import com.android.internal.os.IResultReceiver; -import android.util.MergedConfiguration; /** * API back to a client window that the Window Manager uses to inform it of @@ -139,4 +140,11 @@ oneway interface IWindow { * Tell the window that it is either gaining or losing pointer capture. */ void dispatchPointerCaptureChanged(boolean hasCapture); + + /** + * Called when Scroll Capture support is requested for a window. + * + * @param controller the controller to receive responses + */ + void requestScrollCapture(in IScrollCaptureController controller); } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index b0bacb955f80..b3b53f029382 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -42,6 +42,7 @@ import android.view.IDisplayFoldListener; import android.view.IDisplayWindowRotationController; import android.view.IOnKeyguardExitResult; import android.view.IPinnedStackListener; +import android.view.IScrollCaptureController; import android.view.RemoteAnimationAdapter; import android.view.IRotationWatcher; import android.view.ISystemGestureExclusionListener; @@ -749,4 +750,18 @@ interface IWindowManager * @param flags see definition in SurfaceTracing.cpp */ void setLayerTracingFlags(int flags); + + /** + * Forwards a scroll capture request to the appropriate window, if available. + * + * @param displayId the id of the display to target + * @param behindClient token for a window, used to filter the search to windows behind it, or + * {@code null} to accept a window at any zOrder + * @param taskId specifies the id of a task the result must belong to, or -1 to ignore task ids + * @param controller the controller to receive results, a call to either + * {@link IScrollCaptureController#onClientConnected} or + * {@link IScrollCaptureController#onClientUnavailable}. + */ + void requestScrollCapture(int displayId, IBinder behindClient, int taskId, + IScrollCaptureController controller); } diff --git a/core/java/android/view/ImeInsetsSourceConsumer.java b/core/java/android/view/ImeInsetsSourceConsumer.java index bdc8ba835fbe..df891303bb1d 100644 --- a/core/java/android/view/ImeInsetsSourceConsumer.java +++ b/core/java/android/view/ImeInsetsSourceConsumer.java @@ -153,7 +153,7 @@ public final class ImeInsetsSourceConsumer extends InsetsSourceConsumer { @Override protected boolean isRequestedVisibleAwaitingControl() { - return mIsRequestedVisibleAwaitingControl; + return mIsRequestedVisibleAwaitingControl || isRequestedVisible(); } private boolean isDummyOrEmptyEditor(EditorInfo info) { diff --git a/core/java/android/view/ScrollCaptureCallback.java b/core/java/android/view/ScrollCaptureCallback.java new file mode 100644 index 000000000000..e1a4e7443600 --- /dev/null +++ b/core/java/android/view/ScrollCaptureCallback.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.NonNull; +import android.annotation.UiThread; +import android.graphics.Rect; + +import java.util.function.Consumer; + +/** + * A ScrollCaptureCallback is responsible for providing rendered snapshots of scrolling content for + * the scroll capture system. A single callback is responsible for providing support to a single + * scrolling UI element. At request time, the system will select the best candidate from among all + * callbacks registered within the window. + * <p> + * A callback is assigned to a View using {@link View#setScrollCaptureCallback}, or to the window as + * {@link Window#addScrollCaptureCallback}. The point where the callback is registered defines the + * frame of reference for the bounds measurements used. + * <p> + * <b>Terminology</b> + * <dl> + * <dt>Containing View</dt> + * <dd>The view on which this callback is attached, or the root view of the window if the callback + * is assigned directly to a window.</dd> + * + * <dt>Scroll Bounds</dt> + * <dd>A rectangle which describes an area within the containing view where scrolling content may + * be positioned. This may be the Containing View bounds itself, or any rectangle within. + * Requested by {@link #onScrollCaptureSearch}.</dd> + * + * <dt>Scroll Delta</dt> + * <dd>The distance the scroll position has moved since capture started. Implementations are + * responsible for tracking changes in vertical scroll position during capture. This is required to + * map the capture area to the correct location, given the current scroll position. + * + * <dt>Capture Area</dt> + * <dd>A rectangle which describes the area to capture, relative to scroll bounds. The vertical + * position remains relative to the starting scroll position and any movement since ("Scroll Delta") + * should be subtracted to locate the correct local position, and scrolled into view as necessary. + * </dd> + * </dl> + * + * @see View#setScrollCaptureHint(int) + * @see View#setScrollCaptureCallback(ScrollCaptureCallback) + * @see Window#addScrollCaptureCallback(ScrollCaptureCallback) + * + * @hide + */ +@UiThread +public interface ScrollCaptureCallback { + + /** + * The system is searching for the appropriate scrolling container to capture and would like to + * know the size and position of scrolling content handled by this callback. + * <p> + * Implementations should inset {@code containingViewBounds} to cover only the area within the + * containing view where scrolling content may be positioned. This should cover only the content + * which tracks with scrolling movement. + * <p> + * Return the updated rectangle to {@code resultConsumer}. If for any reason the scrolling + * content is not available to capture, a {@code null} rectangle may be returned, and this view + * will be excluded as the target for this request. + * <p> + * Responses received after XXXms will be discarded. + * <p> + * TODO: finalize timeout + * + * @param onReady consumer for the updated rectangle + */ + void onScrollCaptureSearch(@NonNull Consumer<Rect> onReady); + + /** + * Scroll Capture has selected this callback to provide the scrolling image content. + * <p> + * The onReady signal should be called when ready to begin handling image requests. + */ + void onScrollCaptureStart(@NonNull ScrollCaptureSession session, @NonNull Runnable onReady); + + /** + * An image capture has been requested from the scrolling content. + * <p> + * <code>captureArea</code> contains the bounds of the image requested, relative to the + * rectangle provided by {@link ScrollCaptureCallback#onScrollCaptureSearch}, referred to as + * {@code scrollBounds}. + * here. + * <p> + * A series of requests will step by a constant vertical amount relative to {@code + * scrollBounds}, moving through the scrolling range of content, above and below the current + * visible area. The rectangle's vertical position will not account for any scrolling movement + * since capture started. Implementations therefore must track any scroll position changes and + * subtract this distance from requests. + * <p> + * To handle a request, the content should be scrolled to maximize the visible area of the + * requested rectangle. Offset {@code captureArea} again to account for any further scrolling. + * <p> + * Finally, clip this rectangle against scrollBounds to determine what portion, if any is + * visible content to capture. If the rectangle is completely clipped, set it to {@link + * Rect#setEmpty() empty} and skip the next step. + * <p> + * Make a copy of {@code captureArea}, transform to window coordinates and draw the window, + * clipped to this rectangle, into the {@link ScrollCaptureSession#getSurface() surface} at + * offset (0,0). + * <p> + * Finally, return the resulting {@code captureArea} using + * {@link ScrollCaptureSession#notifyBufferSent}. + * <p> + * If the response is not supplied within XXXms, the session will end with a call to {@link + * #onScrollCaptureEnd}, after which {@code session} is invalid and should be discarded. + * <p> + * TODO: finalize timeout + * <p> + * + * @param captureArea the area to capture, a rectangle within {@code scrollBounds} + */ + void onScrollCaptureImageRequest( + @NonNull ScrollCaptureSession session, @NonNull Rect captureArea); + + /** + * Signals that capture has ended. Implementations should release any temporary resources or + * references to objects in use during the capture. Any resources obtained from the session are + * now invalid and attempts to use them after this point may throw an exception. + * <p> + * The window should be returned as much as possible to its original state when capture started. + * At a minimum, the content should be scrolled to its original position. + * <p> + * <code>onReady</code> should be called when the window should be made visible and + * interactive. The system will wait up to XXXms for this call before proceeding. + * <p> + * TODO: finalize timeout + * + * @param onReady a callback to inform the system that the application has completed any + * cleanup and is ready to become visible + */ + void onScrollCaptureEnd(@NonNull Runnable onReady); +} + diff --git a/core/java/android/view/ScrollCaptureClient.java b/core/java/android/view/ScrollCaptureClient.java new file mode 100644 index 000000000000..f163124f3a98 --- /dev/null +++ b/core/java/android/view/ScrollCaptureClient.java @@ -0,0 +1,312 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.annotation.WorkerThread; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.os.RemoteException; +import android.util.CloseGuard; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A client of the system providing Scroll Capture capability on behalf of a Window. + * <p> + * An instance is created to wrap the selected {@link ScrollCaptureCallback}. + * + * @hide + */ +public class ScrollCaptureClient extends IScrollCaptureClient.Stub { + + private static final String TAG = "ScrollCaptureClient"; + private static final int DEFAULT_TIMEOUT = 1000; + + private final Handler mHandler; + private ScrollCaptureTarget mSelectedTarget; + private int mTimeoutMillis = DEFAULT_TIMEOUT; + + protected Surface mSurface; + private IScrollCaptureController mController; + + private final Rect mScrollBounds; + private final Point mPositionInWindow; + private final CloseGuard mCloseGuard; + + // The current session instance in use by the callback. + private ScrollCaptureSession mSession; + + // Helps manage timeout callbacks registered to handler and aids testing. + private DelayedAction mTimeoutAction; + + /** + * Constructs a ScrollCaptureClient. + * + * @param selectedTarget the target the client is controlling + * @param controller the callbacks to reply to system requests + * + * @hide + */ + public ScrollCaptureClient( + @NonNull ScrollCaptureTarget selectedTarget, + @NonNull IScrollCaptureController controller) { + requireNonNull(selectedTarget, "<selectedTarget> must non-null"); + requireNonNull(controller, "<controller> must non-null"); + final Rect scrollBounds = requireNonNull(selectedTarget.getScrollBounds(), + "target.getScrollBounds() must be non-null to construct a client"); + + mSelectedTarget = selectedTarget; + mHandler = selectedTarget.getContainingView().getHandler(); + mScrollBounds = new Rect(scrollBounds); + mPositionInWindow = new Point(selectedTarget.getPositionInWindow()); + + mController = controller; + mCloseGuard = new CloseGuard(); + mCloseGuard.open("close"); + + selectedTarget.getContainingView().addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + + } + + @Override + public void onViewDetachedFromWindow(View v) { + selectedTarget.getContainingView().removeOnAttachStateChangeListener(this); + endCapture(); + } + }); + } + + @VisibleForTesting + public void setTimeoutMillis(int timeoutMillis) { + mTimeoutMillis = timeoutMillis; + } + + @Nullable + @VisibleForTesting + public DelayedAction getTimeoutAction() { + return mTimeoutAction; + } + + private void checkConnected() { + if (mSelectedTarget == null || mController == null) { + throw new IllegalStateException("This client has been disconnected."); + } + } + + private void checkStarted() { + if (mSession == null) { + throw new IllegalStateException("Capture session has not been started!"); + } + } + + @WorkerThread // IScrollCaptureClient + @Override + public void startCapture(Surface surface) throws RemoteException { + checkConnected(); + mSurface = surface; + scheduleTimeout(mTimeoutMillis, this::onStartCaptureTimeout); + mSession = new ScrollCaptureSession(mSurface, mScrollBounds, mPositionInWindow, this); + mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureStart(mSession, + this::onStartCaptureCompleted)); + } + + @UiThread + private void onStartCaptureCompleted() { + if (cancelTimeout()) { + mHandler.post(() -> { + try { + mController.onCaptureStarted(); + } catch (RemoteException e) { + doShutdown(); + } + }); + } + } + + @UiThread + private void onStartCaptureTimeout() { + endCapture(); + } + + @WorkerThread // IScrollCaptureClient + @Override + public void requestImage(Rect requestRect) { + checkConnected(); + checkStarted(); + scheduleTimeout(mTimeoutMillis, this::onRequestImageTimeout); + // Response is dispatched via ScrollCaptureSession, to onRequestImageCompleted + mHandler.post(() -> mSelectedTarget.getCallback().onScrollCaptureImageRequest( + mSession, new Rect(requestRect))); + } + + @UiThread + void onRequestImageCompleted(long frameNumber, Rect capturedArea) { + final Rect finalCapturedArea = new Rect(capturedArea); + if (cancelTimeout()) { + mHandler.post(() -> { + try { + mController.onCaptureBufferSent(frameNumber, finalCapturedArea); + } catch (RemoteException e) { + doShutdown(); + } + }); + } + } + + @UiThread + private void onRequestImageTimeout() { + endCapture(); + } + + @WorkerThread // IScrollCaptureClient + @Override + public void endCapture() { + if (isStarted()) { + scheduleTimeout(mTimeoutMillis, this::onEndCaptureTimeout); + mHandler.post(() -> + mSelectedTarget.getCallback().onScrollCaptureEnd(this::onEndCaptureCompleted)); + } else { + disconnect(); + } + } + + private boolean isStarted() { + return mController != null && mSelectedTarget != null; + } + + @UiThread + private void onEndCaptureCompleted() { // onEndCaptureCompleted + if (cancelTimeout()) { + doShutdown(); + } + } + + @UiThread + private void onEndCaptureTimeout() { + doShutdown(); + } + + + private void doShutdown() { + try { + if (mController != null) { + mController.onConnectionClosed(); + } + } catch (RemoteException e) { + // Ignore + } finally { + disconnect(); + } + } + + /** + * Shuts down this client and releases references to dependent objects. No attempt is made + * to notify the controller, use with caution! + */ + public void disconnect() { + if (mSession != null) { + mSession.disconnect(); + mSession = null; + } + + mSelectedTarget = null; + mController = null; + } + + /** @return a string representation of the state of this client */ + public String toString() { + return "ScrollCaptureClient{" + + ", session=" + mSession + + ", selectedTarget=" + mSelectedTarget + + ", clientCallbacks=" + mController + + "}"; + } + + private boolean cancelTimeout() { + if (mTimeoutAction != null) { + return mTimeoutAction.cancel(); + } + return false; + } + + private void scheduleTimeout(long timeoutMillis, Runnable action) { + if (mTimeoutAction != null) { + mTimeoutAction.cancel(); + } + mTimeoutAction = new DelayedAction(mHandler, timeoutMillis, action); + } + + /** @hide */ + @VisibleForTesting + public static class DelayedAction { + private final AtomicBoolean mCompleted = new AtomicBoolean(); + private final Object mToken = new Object(); + private final Handler mHandler; + private final Runnable mAction; + + @VisibleForTesting + public DelayedAction(Handler handler, long timeoutMillis, Runnable action) { + mHandler = handler; + mAction = action; + mHandler.postDelayed(this::onTimeout, mToken, timeoutMillis); + } + + private boolean onTimeout() { + if (mCompleted.compareAndSet(false, true)) { + mAction.run(); + return true; + } + return false; + } + + /** + * Cause the timeout action to run immediately and mark as timed out. + * + * @return true if the timeout was run, false if the timeout had already been canceled + */ + @VisibleForTesting + public boolean timeoutNow() { + return onTimeout(); + } + + /** + * Attempt to cancel the timeout action (such as after a callback is made) + * + * @return true if the timeout was canceled and will not run, false if time has expired and + * the timeout action has or will run momentarily + */ + public boolean cancel() { + if (!mCompleted.compareAndSet(false, true)) { + // Whoops, too late! + return false; + } + mHandler.removeCallbacksAndMessages(mToken); + return true; + } + } +} diff --git a/core/java/android/view/ScrollCaptureSession.java b/core/java/android/view/ScrollCaptureSession.java new file mode 100644 index 000000000000..628e23fb3f5e --- /dev/null +++ b/core/java/android/view/ScrollCaptureSession.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Point; +import android.graphics.Rect; + +/** + * A session represents the scope of interaction between a {@link ScrollCaptureCallback} and the + * system during an active scroll capture operation. During the scope of a session, a callback + * will receive a series of requests for image data. Resources provided here are valid for use + * until {@link ScrollCaptureCallback#onScrollCaptureEnd(Runnable)}. + * + * @hide + */ +public class ScrollCaptureSession { + + private final Surface mSurface; + private final Rect mScrollBounds; + private final Point mPositionInWindow; + + @Nullable + private ScrollCaptureClient mClient; + + /** @hide */ + public ScrollCaptureSession(Surface surface, Rect scrollBounds, Point positionInWindow, + ScrollCaptureClient client) { + mSurface = surface; + mScrollBounds = scrollBounds; + mPositionInWindow = positionInWindow; + mClient = client; + } + + /** + * Returns a + * <a href="https://source.android.com/devices/graphics/arch-bq-gralloc">BufferQueue</a> in the + * form of a {@link Surface} for transfer of image buffers. + * + * @return the surface for transferring image buffers + * @throws IllegalStateException if the session has been closed + */ + @NonNull + public Surface getSurface() { + return mSurface; + } + + /** + * Returns the {@code scroll bounds}, as provided by + * {@link ScrollCaptureCallback#onScrollCaptureSearch}. + * + * @return the area of scrolling content within the containing view + */ + @NonNull + public Rect getScrollBounds() { + return mScrollBounds; + } + + /** + * Returns the offset of {@code scroll bounds} within the window. + * + * @return the area of scrolling content within the containing view + */ + @NonNull + public Point getPositionInWindow() { + return mPositionInWindow; + } + + /** + * Notify the system that an a buffer has been posted via the getSurface() channel. + * + * @param frameNumber the frame number of the queued buffer + * @param capturedArea the area captured, relative to scroll bounds + */ + public void notifyBufferSent(long frameNumber, @NonNull Rect capturedArea) { + if (mClient != null) { + mClient.onRequestImageCompleted(frameNumber, capturedArea); + } + } + + /** + * @hide + */ + public void disconnect() { + mClient = null; + if (mSurface.isValid()) { + mSurface.release(); + } + } +} diff --git a/core/java/android/view/ScrollCaptureTarget.java b/core/java/android/view/ScrollCaptureTarget.java new file mode 100644 index 000000000000..f3fcabb26b31 --- /dev/null +++ b/core/java/android/view/ScrollCaptureTarget.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.graphics.Matrix; +import android.graphics.Point; +import android.graphics.Rect; + +import com.android.internal.util.FastMath; + +/** + * A target collects the set of contextual information for a ScrollCaptureHandler discovered during + * a {@link View#dispatchScrollCaptureSearch scroll capture search}. + * + * @hide + */ +public final class ScrollCaptureTarget { + private final View mContainingView; + private final ScrollCaptureCallback mCallback; + private final Rect mLocalVisibleRect; + private final Point mPositionInWindow; + private final int mHint; + private Rect mScrollBounds; + + private final float[] mTmpFloatArr = new float[2]; + private final Matrix mMatrixViewLocalToWindow = new Matrix(); + private final Rect mTmpRect = new Rect(); + + public ScrollCaptureTarget(@NonNull View scrollTarget, @NonNull Rect localVisibleRect, + @NonNull Point positionInWindow, @NonNull ScrollCaptureCallback callback) { + mContainingView = scrollTarget; + mHint = mContainingView.getScrollCaptureHint(); + mCallback = callback; + mLocalVisibleRect = localVisibleRect; + mPositionInWindow = positionInWindow; + } + + /** @return the hint that the {@code containing view} had during the scroll capture search */ + @View.ScrollCaptureHint + public int getHint() { + return mHint; + } + + /** @return the {@link ScrollCaptureCallback} for this target */ + @NonNull + public ScrollCaptureCallback getCallback() { + return mCallback; + } + + /** @return the {@code containing view} for this {@link ScrollCaptureCallback callback} */ + @NonNull + public View getContainingView() { + return mContainingView; + } + + /** + * Returns the un-clipped, visible bounds of the containing view during the scroll capture + * search. This is used to determine on-screen area to assist in selecting the primary target. + * + * @return the visible bounds of the {@code containing view} in view-local coordinates + */ + @NonNull + public Rect getLocalVisibleRect() { + return mLocalVisibleRect; + } + + /** @return the position of the {@code containing view} within the window */ + @NonNull + public Point getPositionInWindow() { + return mPositionInWindow; + } + + /** @return the {@code scroll bounds} for this {@link ScrollCaptureCallback callback} */ + @Nullable + public Rect getScrollBounds() { + return mScrollBounds; + } + + /** + * Sets the scroll bounds rect to the intersection of provided rect and the current bounds of + * the {@code containing view}. + */ + public void setScrollBounds(@Nullable Rect scrollBounds) { + mScrollBounds = Rect.copyOrNull(scrollBounds); + if (mScrollBounds == null) { + return; + } + if (!mScrollBounds.intersect(0, 0, + mContainingView.getWidth(), mContainingView.getHeight())) { + mScrollBounds.setEmpty(); + } + } + + private static void zero(float[] pointArray) { + pointArray[0] = 0; + pointArray[1] = 0; + } + + private static void roundIntoPoint(Point pointObj, float[] pointArray) { + pointObj.x = FastMath.round(pointArray[0]); + pointObj.y = FastMath.round(pointArray[1]); + } + + /** + * Refresh the value of {@link #mLocalVisibleRect} and {@link #mPositionInWindow} based on the + * current state of the {@code containing view}. + */ + @UiThread + public void updatePositionInWindow() { + mMatrixViewLocalToWindow.reset(); + mContainingView.transformMatrixToGlobal(mMatrixViewLocalToWindow); + + zero(mTmpFloatArr); + mMatrixViewLocalToWindow.mapPoints(mTmpFloatArr); + roundIntoPoint(mPositionInWindow, mTmpFloatArr); + } + +} diff --git a/core/java/android/view/ScrollCaptureTargetResolver.java b/core/java/android/view/ScrollCaptureTargetResolver.java new file mode 100644 index 000000000000..71e82c511e2c --- /dev/null +++ b/core/java/android/view/ScrollCaptureTargetResolver.java @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.annotation.AnyThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UiThread; +import android.graphics.Rect; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + + +import java.util.Queue; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * Queries additional state from a list of {@link ScrollCaptureTarget targets} via asynchronous + * callbacks, then aggregates and reduces the target list to a single target, or null if no target + * is suitable. + * <p> + * The rules for selection are (in order): + * <ul> + * <li>prefer getScrollBounds(): non-empty + * <li>prefer View.getScrollCaptureHint == SCROLL_CAPTURE_HINT_INCLUDE + * <li>prefer descendants before parents + * <li>prefer larger area for getScrollBounds() (clipped to view bounds) + * </ul> + * + * <p> + * All calls to {@link ScrollCaptureCallback#onScrollCaptureSearch} are made on the main thread, + * with results are queued and consumed to the main thread as well. + * + * @see #start(Handler, long, Consumer) + * + * @hide + */ +@UiThread +public class ScrollCaptureTargetResolver { + private static final String TAG = "ScrollCaptureTargetRes"; + private static final boolean DEBUG = true; + + private final Object mLock = new Object(); + + private final Queue<ScrollCaptureTarget> mTargets; + private Handler mHandler; + private long mTimeLimitMillis; + + private Consumer<ScrollCaptureTarget> mWhenComplete; + private int mPendingBoundsRequests; + private long mDeadlineMillis; + + private ScrollCaptureTarget mResult; + private boolean mFinished; + + private boolean mStarted; + + private static int area(Rect r) { + return r.width() * r.height(); + } + + private static boolean nullOrEmpty(Rect r) { + return r == null || r.isEmpty(); + } + + /** + * Binary operator which selects the best {@link ScrollCaptureTarget}. + */ + private static ScrollCaptureTarget chooseTarget(ScrollCaptureTarget a, ScrollCaptureTarget b) { + Log.d(TAG, "chooseTarget: " + a + " or " + b); + // Nothing plus nothing is still nothing. + if (a == null && b == null) { + Log.d(TAG, "chooseTarget: (both null) return " + null); + return null; + } + // Prefer non-null. + if (a == null || b == null) { + ScrollCaptureTarget c = (a == null) ? b : a; + Log.d(TAG, "chooseTarget: (other is null) return " + c); + return c; + + } + + boolean emptyScrollBoundsA = nullOrEmpty(a.getScrollBounds()); + boolean emptyScrollBoundsB = nullOrEmpty(b.getScrollBounds()); + if (emptyScrollBoundsA || emptyScrollBoundsB) { + if (emptyScrollBoundsA && emptyScrollBoundsB) { + // Both have an empty or null scrollBounds + Log.d(TAG, "chooseTarget: (both have empty or null bounds) return " + null); + return null; + } + // Prefer the one with a non-empty scroll bounds + if (emptyScrollBoundsA) { + Log.d(TAG, "chooseTarget: (a has empty or null bounds) return " + b); + return b; + } + Log.d(TAG, "chooseTarget: (b has empty or null bounds) return " + a); + return a; + } + + final View viewA = a.getContainingView(); + final View viewB = b.getContainingView(); + + // Prefer any view with scrollCaptureHint="INCLUDE", over one without + // This is an escape hatch for the next rule (descendants first) + boolean hintIncludeA = hasIncludeHint(viewA); + boolean hintIncludeB = hasIncludeHint(viewB); + if (hintIncludeA != hintIncludeB) { + ScrollCaptureTarget c = (hintIncludeA) ? a : b; + Log.d(TAG, "chooseTarget: (has hint=INCLUDE) return " + c); + return c; + } + + // If the views are relatives, prefer the descendant. This allows implementations to + // leverage nested scrolling APIs by interacting with the innermost scrollable view (as + // would happen with touch input). + if (isDescendant(viewA, viewB)) { + Log.d(TAG, "chooseTarget: (b is descendant of a) return " + b); + return b; + } + if (isDescendant(viewB, viewA)) { + Log.d(TAG, "chooseTarget: (a is descendant of b) return " + a); + return a; + } + + // finally, prefer one with larger scroll bounds + int scrollAreaA = area(a.getScrollBounds()); + int scrollAreaB = area(b.getScrollBounds()); + ScrollCaptureTarget c = (scrollAreaA >= scrollAreaB) ? a : b; + Log.d(TAG, "chooseTarget: return " + c); + return c; + } + + /** + * Creates an instance to query and filter {@code target}. + * + * @param targets a list of {@link ScrollCaptureTarget} as collected by {@link + * View#dispatchScrollCaptureSearch}. + * @param uiHandler the UI thread handler for the view tree + * @see #start(long, Consumer) + */ + public ScrollCaptureTargetResolver(Queue<ScrollCaptureTarget> targets) { + mTargets = targets; + } + + void checkThread() { + if (mHandler.getLooper() != Looper.myLooper()) { + throw new IllegalStateException("Called from wrong thread! (" + + Thread.currentThread().getName() + ")"); + } + } + + /** + * Blocks until a result is returned (after completion or timeout). + * <p> + * For testing only. Normal usage should receive a callback after calling {@link #start}. + */ + @VisibleForTesting + public ScrollCaptureTarget waitForResult() throws InterruptedException { + synchronized (mLock) { + while (!mFinished) { + mLock.wait(); + } + } + return mResult; + } + + + private void supplyResult(ScrollCaptureTarget target) { + checkThread(); + if (mFinished) { + return; + } + mResult = chooseTarget(mResult, target); + boolean finish = mPendingBoundsRequests == 0 + || SystemClock.elapsedRealtime() >= mDeadlineMillis; + if (finish) { + System.err.println("We think we're done, or timed out"); + mPendingBoundsRequests = 0; + mWhenComplete.accept(mResult); + synchronized (mLock) { + mFinished = true; + mLock.notify(); + } + mWhenComplete = null; + } + } + + /** + * Asks all targets for {@link ScrollCaptureCallback#onScrollCaptureSearch(Consumer) + * scrollBounds}, and selects the primary target according to the {@link + * #chooseTarget} function. + * + * @param timeLimitMillis the amount of time to wait for all responses before delivering the top + * result + * @param resultConsumer the consumer to receive the primary target + */ + @AnyThread + public void start(Handler uiHandler, long timeLimitMillis, + Consumer<ScrollCaptureTarget> resultConsumer) { + synchronized (mLock) { + if (mStarted) { + throw new IllegalStateException("already started!"); + } + if (timeLimitMillis < 0) { + throw new IllegalArgumentException("Time limit must be positive"); + } + mHandler = uiHandler; + mTimeLimitMillis = timeLimitMillis; + mWhenComplete = resultConsumer; + if (mTargets.isEmpty()) { + mHandler.post(() -> supplyResult(null)); + return; + } + mStarted = true; + uiHandler.post(() -> run(timeLimitMillis, resultConsumer)); + } + } + + + private void run(long timeLimitMillis, Consumer<ScrollCaptureTarget> resultConsumer) { + checkThread(); + + mPendingBoundsRequests = mTargets.size(); + for (ScrollCaptureTarget target : mTargets) { + queryTarget(target); + } + mDeadlineMillis = SystemClock.elapsedRealtime() + mTimeLimitMillis; + mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis); + } + + private final Runnable mTimeoutRunnable = new Runnable() { + @Override + public void run() { + checkThread(); + supplyResult(null); + } + }; + + + /** + * Adds a target to the list and requests {@link ScrollCaptureCallback#onScrollCaptureSearch} + * scrollBounds} from it. Results are returned by a call to {@link #onScrollBoundsProvided}. + * + * @param target the target to add + */ + @UiThread + private void queryTarget(@NonNull ScrollCaptureTarget target) { + checkThread(); + final ScrollCaptureCallback callback = target.getCallback(); + // from the UI thread, request scroll bounds + callback.onScrollCaptureSearch( + // allow only one callback to onReady.accept(): + new SingletonConsumer<Rect>( + // Queue and consume on the UI thread + ((scrollBounds) -> mHandler.post( + () -> onScrollBoundsProvided(target, scrollBounds))))); + + } + + @UiThread + private void onScrollBoundsProvided(ScrollCaptureTarget target, @Nullable Rect scrollBounds) { + checkThread(); + if (mFinished) { + return; + } + + // Record progress. + mPendingBoundsRequests--; + + // Remove the timeout. + mHandler.removeCallbacks(mTimeoutRunnable); + + boolean doneOrTimedOut = mPendingBoundsRequests == 0 + || SystemClock.elapsedRealtime() >= mDeadlineMillis; + + final View containingView = target.getContainingView(); + if (!nullOrEmpty(scrollBounds) && containingView.isAggregatedVisible()) { + target.updatePositionInWindow(); + target.setScrollBounds(scrollBounds); + supplyResult(target); + } + + System.err.println("mPendingBoundsRequests: " + mPendingBoundsRequests); + System.err.println("mDeadlineMillis: " + mDeadlineMillis); + System.err.println("SystemClock.elapsedRealtime(): " + SystemClock.elapsedRealtime()); + + if (!mFinished) { + // Reschedule the timeout. + System.err.println( + "We think we're NOT done yet and will check back at " + mDeadlineMillis); + mHandler.postAtTime(mTimeoutRunnable, mDeadlineMillis); + } + } + + private static boolean hasIncludeHint(View view) { + return (view.getScrollCaptureHint() & View.SCROLL_CAPTURE_HINT_INCLUDE) != 0; + } + + /** + * Determines if {@code otherView} is a descendant of {@code view}. + * + * @param view a view + * @param otherView another view + * @return true if {@code view} is an ancestor of {@code otherView} + */ + private static boolean isDescendant(@NonNull View view, @NonNull View otherView) { + if (view == otherView) { + return false; + } + ViewParent otherParent = otherView.getParent(); + while (otherParent != view && otherParent != null) { + otherParent = otherParent.getParent(); + } + return otherParent == view; + } + + private static int findRelation(@NonNull View a, @NonNull View b) { + if (a == b) { + return 0; + } + + ViewParent parentA = a.getParent(); + ViewParent parentB = b.getParent(); + + while (parentA != null || parentB != null) { + if (parentA == parentB) { + return 0; + } + if (parentA == b) { + return 1; // A is descendant of B + } + if (parentB == a) { + return -1; // B is descendant of A + } + if (parentA != null) { + parentA = parentA.getParent(); + } + if (parentB != null) { + parentB = parentB.getParent(); + } + } + return 0; + } + + /** + * A safe wrapper for a consumer callbacks intended to accept a single value. It ensures + * that the receiver of the consumer does not retain a reference to {@code target} after use nor + * cause race conditions by invoking {@link Consumer#accept accept} more than once. + * + * @param target the target consumer + */ + static class SingletonConsumer<T> implements Consumer<T> { + final AtomicReference<Consumer<T>> mAtomicRef; + + SingletonConsumer(Consumer<T> target) { + mAtomicRef = new AtomicReference<>(target); + } + + @Override + public void accept(T t) { + final Consumer<T> consumer = mAtomicRef.getAndSet(null); + if (consumer != null) { + consumer.accept(t); + } + } + } +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 8abe72fc91e8..f98c1f660cfa 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -144,6 +144,7 @@ import android.widget.ScrollBarDrawable; import com.android.internal.R; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.view.ScrollCaptureInternal; import com.android.internal.view.TooltipPopup; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.widget.ScrollBarUtils; @@ -167,6 +168,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Queue; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; @@ -1311,7 +1313,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int AUTOFILL_TYPE_LIST = 3; - /** * Autofill type for a field that contains a date, which is represented by a long representing * the number of milliseconds since the standard base time known as "the epoch", namely @@ -1441,6 +1442,58 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ public static final int IMPORTANT_FOR_CONTENT_CAPTURE_NO_EXCLUDE_DESCENDANTS = 0x8; + /** {@hide} */ + @IntDef(flag = true, prefix = {"SCROLL_CAPTURE_HINT_"}, + value = { + SCROLL_CAPTURE_HINT_AUTO, + SCROLL_CAPTURE_HINT_EXCLUDE, + SCROLL_CAPTURE_HINT_INCLUDE, + SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScrollCaptureHint {} + + /** + * The content of this view will be considered for scroll capture if scrolling is possible. + * + * @see #getScrollCaptureHint() + * @see #setScrollCaptureHint(int) + * @hide + */ + public static final int SCROLL_CAPTURE_HINT_AUTO = 0; + + /** + * Explicitly exclcude this view as a potential scroll capture target. The system will not + * consider it. Mutually exclusive with {@link #SCROLL_CAPTURE_HINT_INCLUDE}, which this flag + * takes precedence over. + * + * @see #getScrollCaptureHint() + * @see #setScrollCaptureHint(int) + * @hide + */ + public static final int SCROLL_CAPTURE_HINT_EXCLUDE = 0x1; + + /** + * Explicitly include this view as a potential scroll capture target. When locating a scroll + * capture target, this view will be prioritized before others without this flag. Mutually + * exclusive with {@link #SCROLL_CAPTURE_HINT_EXCLUDE}, which takes precedence. + * + * @see #getScrollCaptureHint() + * @see #setScrollCaptureHint(int) + * @hide + */ + public static final int SCROLL_CAPTURE_HINT_INCLUDE = 0x2; + + /** + * Explicitly exclude all children of this view as potential scroll capture targets. This view + * is unaffected. Note: Excluded children are not considered, regardless of {@link + * #SCROLL_CAPTURE_HINT_INCLUDE}. + * + * @see #getScrollCaptureHint() + * @see #setScrollCaptureHint(int) + * @hide + */ + public static final int SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS = 0x4; /** * This view is enabled. Interpretation varies by subclass. @@ -3430,6 +3483,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * 11 PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK * 1 PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS * 1 PFLAG4_AUTOFILL_HIDE_HIGHLIGHT + * 11 PFLAG4_SCROLL_CAPTURE_HINT_MASK * |-------|-------|-------|-------| */ @@ -3477,6 +3531,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final int PFLAG4_AUTOFILL_HIDE_HIGHLIGHT = 0x200; + /** + * Shift for the bits in {@link #mPrivateFlags4} related to scroll capture. + */ + static final int PFLAG4_SCROLL_CAPTURE_HINT_SHIFT = 10; + + static final int PFLAG4_SCROLL_CAPTURE_HINT_MASK = (SCROLL_CAPTURE_HINT_INCLUDE + | SCROLL_CAPTURE_HINT_EXCLUDE | SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS) + << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT; + /* End of masks for mPrivateFlags4 */ /** @hide */ @@ -4690,6 +4753,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Used to track {@link #mSystemGestureExclusionRects} */ public RenderNode.PositionUpdateListener mPositionUpdateListener; + + /** + * Allows the application to implement custom scroll capture support. + */ + ScrollCaptureCallback mScrollCaptureCallback; } @UnsupportedAppUsage @@ -5941,6 +6009,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case R.styleable.View_forceDarkAllowed: mRenderNode.setForceDarkAllowed(a.getBoolean(attr, true)); break; + case R.styleable.View_scrollCaptureHint: + setScrollCaptureHint((a.getInt(attr, SCROLL_CAPTURE_HINT_AUTO))); + break; } } @@ -29091,6 +29162,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, int mLeashedParentAccessibilityViewId; /** + * + */ + ScrollCaptureInternal mScrollCaptureInternal; + + /** * Creates a new set of attachment information with the specified * events handler and thread. * @@ -29150,6 +29226,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return events; } + + @Nullable + ScrollCaptureInternal getScrollCaptureInternal() { + if (mScrollCaptureInternal != null) { + mScrollCaptureInternal = new ScrollCaptureInternal(); + } + return mScrollCaptureInternal; + } } /** @@ -29683,6 +29767,104 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + + /** + * Returns the current scroll capture hint for this view. + * + * @return the current scroll capture hint + * + * @hide + */ + @ScrollCaptureHint + public int getScrollCaptureHint() { + return (mPrivateFlags4 & PFLAG4_SCROLL_CAPTURE_HINT_MASK) + >> PFLAG4_SCROLL_CAPTURE_HINT_SHIFT; + } + + /** + * Sets the scroll capture hint for this View. These flags affect the search for a potential + * scroll capture targets. + * + * @param hint the scrollCaptureHint flags value to set + * + * @hide + */ + public void setScrollCaptureHint(@ScrollCaptureHint int hint) { + mPrivateFlags4 &= ~PFLAG4_SCROLL_CAPTURE_HINT_MASK; + mPrivateFlags4 |= ((hint << PFLAG4_SCROLL_CAPTURE_HINT_SHIFT) + & PFLAG4_SCROLL_CAPTURE_HINT_MASK); + } + + /** + * Sets the callback to receive scroll capture requests. This component is the adapter between + * the scroll capture API and application UI code. If no callback is set, the system may provide + * an implementation. Any value provided here will take precedence over a system version. + * <p> + * This view will be ignored when {@link #SCROLL_CAPTURE_HINT_EXCLUDE} is set in its {@link + * #setScrollCaptureHint(int) scrollCaptureHint}, regardless whether a callback has been set. + * <p> + * It is recommended to set the scroll capture hint {@link #SCROLL_CAPTURE_HINT_INCLUDE} when + * setting a custom callback to help ensure it is selected as the target. + * + * @param callback the new callback to assign + * + * @hide + */ + public void setScrollCaptureCallback(@Nullable ScrollCaptureCallback callback) { + getListenerInfo().mScrollCaptureCallback = callback; + } + + /** {@hide} */ + @Nullable + public ScrollCaptureCallback createScrollCaptureCallbackInternal(@NonNull Rect localVisibleRect, + @NonNull Point windowOffset) { + if (mAttachInfo == null) { + return null; + } + if (mAttachInfo.mScrollCaptureInternal == null) { + mAttachInfo.mScrollCaptureInternal = new ScrollCaptureInternal(); + } + return mAttachInfo.mScrollCaptureInternal.requestCallback(this, localVisibleRect, + windowOffset); + } + + /** + * Called when scroll capture is requested, to search for appropriate content to scroll. If + * applicable, this view adds itself to the provided list for consideration, subject to the + * flags set by {@link #setScrollCaptureHint}. + * + * @param localVisibleRect the local visible rect of this view + * @param windowOffset the offset of localVisibleRect within the window + * @param targets a queue which collects potential targets + * + * @throws IllegalStateException if this view is not attached to a window + * @hide + */ + public void dispatchScrollCaptureSearch(@NonNull Rect localVisibleRect, + @NonNull Point windowOffset, @NonNull Queue<ScrollCaptureTarget> targets) { + int hint = getScrollCaptureHint(); + if ((hint & SCROLL_CAPTURE_HINT_EXCLUDE) != 0) { + return; + } + + // Get a callback provided by the framework, library or application. + ScrollCaptureCallback callback = + (mListenerInfo == null) ? null : mListenerInfo.mScrollCaptureCallback; + + // Try internal support for standard scrolling containers. + if (callback == null) { + callback = createScrollCaptureCallbackInternal(localVisibleRect, windowOffset); + } + + // If found, then add it to the list. + if (callback != null) { + // Add to the list for consideration + Point offset = new Point(windowOffset.x, windowOffset.y); + Rect rect = new Rect(localVisibleRect); + targets.add(new ScrollCaptureTarget(this, rect, offset, callback)); + } + } + /** * Dump all private flags in readable format, useful for documentation and * sanity checking. diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index e34e84c977ea..7935eb1ffc39 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -40,6 +40,7 @@ import android.graphics.Color; import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Paint; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; @@ -75,6 +76,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.function.Predicate; /** @@ -188,7 +190,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private PointF mLocalPoint; // Lazily-created holder for point computations. - private float[] mTempPoint; + private float[] mTempPosition; + + // Lazily-created holder for point computations. + private Point mTempPoint; + + // Lazily created Rect for dispatch to children + private Rect mTempRect; + + // Lazily created int[2] for dispatch to children + private int[] mTempLocation; // Layout animation private LayoutAnimationController mLayoutAnimationController; @@ -1860,7 +1871,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final float tx = mCurrentDragStartEvent.mX; final float ty = mCurrentDragStartEvent.mY; - final float[] point = getTempPoint(); + final float[] point = getTempLocationF(); point[0] = tx; point[1] = ty; transformPointToViewLocal(point, child); @@ -2932,9 +2943,23 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } - private float[] getTempPoint() { + private Rect getTempRect() { + if (mTempRect == null) { + mTempRect = new Rect(); + } + return mTempRect; + } + + private float[] getTempLocationF() { + if (mTempPosition == null) { + mTempPosition = new float[2]; + } + return mTempPosition; + } + + private Point getTempPoint() { if (mTempPoint == null) { - mTempPoint = new float[2]; + mTempPoint = new Point(); } return mTempPoint; } @@ -2948,7 +2973,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @UnsupportedAppUsage protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) { - final float[] point = getTempPoint(); + final float[] point = getTempLocationF(); point[0] = x; point[1] = y; transformPointToViewLocal(point, child); @@ -4568,7 +4593,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final boolean nonActionable = !child.isClickable() && !child.isLongClickable(); final boolean duplicatesState = (child.mViewFlags & DUPLICATE_PARENT_STATE) != 0; if (nonActionable || duplicatesState) { - final float[] point = getTempPoint(); + final float[] point = getTempLocationF(); point[0] = x; point[1] = y; transformPointToViewLocal(point, child); @@ -7354,6 +7379,97 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Offsets the given rectangle in parent's local coordinates into child's coordinate space + * and clips the result to the child View's bounds, padding and clipRect if appropriate. If the + * resulting rectangle is not empty, the request is forwarded to the child. + * <p> + * Note: This method does not account for any static View transformations which may be + * applied to the child view. + * + * @param child the child to dispatch to + * @param localVisibleRect the visible (clipped) area of this ViewGroup, in local coordinates + * @param windowOffset the offset of localVisibleRect within the window + * @param targets a queue to collect located targets + */ + private void dispatchTransformedScrollCaptureSearch(View child, Rect localVisibleRect, + Point windowOffset, Queue<ScrollCaptureTarget> targets) { + + // copy local visible rect for modification and dispatch + final Rect childVisibleRect = getTempRect(); + childVisibleRect.set(localVisibleRect); + + // transform to child coords + final Point childWindowOffset = getTempPoint(); + childWindowOffset.set(windowOffset.x, windowOffset.y); + + final int dx = child.mLeft - mScrollX; + final int dy = child.mTop - mScrollY; + + childVisibleRect.offset(-dx, -dy); + childWindowOffset.offset(dx, dy); + + boolean rectIsVisible = true; + final int width = mRight - mLeft; + final int height = mBottom - mTop; + + // Clip to child bounds + if (getClipChildren()) { + rectIsVisible = childVisibleRect.intersect(0, 0, child.getWidth(), child.getHeight()); + } + + // Clip to child padding. + if (rectIsVisible && (child instanceof ViewGroup) + && ((ViewGroup) child).getClipToPadding()) { + rectIsVisible = childVisibleRect.intersect( + child.mPaddingLeft, child.mPaddingTop, + child.getWidth() - child.mPaddingRight, + child.getHeight() - child.mPaddingBottom); + } + // Clip to child clipBounds. + if (rectIsVisible && child.mClipBounds != null) { + rectIsVisible = childVisibleRect.intersect(child.mClipBounds); + } + if (rectIsVisible) { + child.dispatchScrollCaptureSearch(childVisibleRect, childWindowOffset, targets); + } + } + + /** + * Handle the scroll capture search request by checking this view if applicable, then to each + * child view. + * + * @param localVisibleRect the visible area of this ViewGroup in local coordinates, according to + * the parent + * @param windowOffset the offset of this view within the window + * @param targets the collected list of scroll capture targets + * + * @hide + */ + @Override + public void dispatchScrollCaptureSearch( + @NonNull Rect localVisibleRect, @NonNull Point windowOffset, + @NonNull Queue<ScrollCaptureTarget> targets) { + + // Dispatch to self first. + super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets); + + // Then dispatch to children, if not excluding descendants. + if ((getScrollCaptureHint() & SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS) == 0) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + // Only visible views can be captured. + if (child.getVisibility() != View.VISIBLE) { + continue; + } + // Transform to child coords and dispatch + dispatchTransformedScrollCaptureSearch(child, localVisibleRect, windowOffset, + targets); + } + } + } + + /** * Returns the animation listener to which layout animation events are * sent. * diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index ed1edc3bd526..9d275cdcb00f 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -206,6 +206,7 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_INPUT_STAGES = false || LOCAL_LOGV; private static final boolean DEBUG_KEEP_SCREEN_ON = false || LOCAL_LOGV; private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV; + private static final boolean DEBUG_SCROLL_CAPTURE = false || LOCAL_LOGV; /** * Set to false if we do not want to use the multi threaded renderer even though @@ -653,6 +654,8 @@ public final class ViewRootImpl implements ViewParent, private final InsetsController mInsetsController; private final ImeFocusController mImeFocusController; + private ScrollCaptureClient mScrollCaptureClient; + /** * @return {@link ImeFocusController} for this instance. */ @@ -661,6 +664,11 @@ public final class ViewRootImpl implements ViewParent, return mImeFocusController; } + /** @return The current {@link ScrollCaptureClient} for this instance, if any is active. */ + @Nullable + public ScrollCaptureClient getScrollCaptureClient() { + return mScrollCaptureClient; + } private final GestureExclusionTracker mGestureExclusionTracker = new GestureExclusionTracker(); @@ -694,6 +702,8 @@ public final class ViewRootImpl implements ViewParent, // draw returns. private SurfaceControl.Transaction mRtBLASTSyncTransaction = new SurfaceControl.Transaction(); + private HashSet<ScrollCaptureCallback> mRootScrollCaptureCallbacks; + private String mTag = TAG; public ViewRootImpl(Context context, Display display) { @@ -3769,7 +3779,9 @@ public final class ViewRootImpl implements ViewParent, mNextReportConsumeBLAST = true; mNextDrawUseBLASTSyncTransaction = false; - mBlastBufferQueue.setNextTransaction(mRtBLASTSyncTransaction); + if (mBlastBufferQueue != null) { + mBlastBufferQueue.setNextTransaction(mRtBLASTSyncTransaction); + } } boolean canUseAsync = draw(fullRedrawNeeded); if (usingAsyncReport && !canUseAsync) { @@ -4778,6 +4790,7 @@ public final class ViewRootImpl implements ViewParent, private static final int MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED = 33; private static final int MSG_SHOW_INSETS = 34; private static final int MSG_HIDE_INSETS = 35; + private static final int MSG_REQUEST_SCROLL_CAPTURE = 36; final class ViewRootHandler extends Handler { @@ -5080,6 +5093,9 @@ public final class ViewRootImpl implements ViewParent, case MSG_LOCATION_IN_PARENT_DISPLAY_CHANGED: { updateLocationInParentDisplay(msg.arg1, msg.arg2); } break; + case MSG_REQUEST_SCROLL_CAPTURE: + handleScrollCaptureRequest((IScrollCaptureController) msg.obj); + break; } } } @@ -8789,6 +8805,131 @@ public final class ViewRootImpl implements ViewParent, return false; } + /** + * Adds a scroll capture callback to this window. + * + * @param callback the callback to add + */ + public void addScrollCaptureCallback(ScrollCaptureCallback callback) { + if (mRootScrollCaptureCallbacks == null) { + mRootScrollCaptureCallbacks = new HashSet<>(); + } + mRootScrollCaptureCallbacks.add(callback); + } + + /** + * Removes a scroll capture callback from this window. + * + * @param callback the callback to remove + */ + public void removeScrollCaptureCallback(ScrollCaptureCallback callback) { + if (mRootScrollCaptureCallbacks != null) { + mRootScrollCaptureCallbacks.remove(callback); + if (mRootScrollCaptureCallbacks.isEmpty()) { + mRootScrollCaptureCallbacks = null; + } + } + } + + /** + * Dispatches a scroll capture request to the view hierarchy on the ui thread. + * + * @param controller the controller to receive replies + */ + public void dispatchScrollCaptureRequest(@NonNull IScrollCaptureController controller) { + mHandler.obtainMessage(MSG_REQUEST_SCROLL_CAPTURE, controller).sendToTarget(); + } + + /** + * Collect and include any ScrollCaptureCallback instances registered with the window. + * + * @see #addScrollCaptureCallback(ScrollCaptureCallback) + * @param targets the search queue for targets + */ + private void collectRootScrollCaptureTargets(Queue<ScrollCaptureTarget> targets) { + for (ScrollCaptureCallback cb : mRootScrollCaptureCallbacks) { + // Add to the list for consideration + Point offset = new Point(mView.getLeft(), mView.getTop()); + Rect rect = new Rect(0, 0, mView.getWidth(), mView.getHeight()); + targets.add(new ScrollCaptureTarget(mView, rect, offset, cb)); + } + } + + /** + * Handles an inbound request for scroll capture from the system. If a client is not already + * active, a search will be dispatched through the view tree to locate scrolling content. + * <p> + * Either {@link IScrollCaptureController#onClientConnected(IScrollCaptureClient, Rect, + * Point)} or {@link IScrollCaptureController#onClientUnavailable()} will be returned + * depending on the results of the search. + * + * @param controller the interface to the system controller + * @see ScrollCaptureTargetResolver + */ + private void handleScrollCaptureRequest(@NonNull IScrollCaptureController controller) { + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Window (root) level callbacks + collectRootScrollCaptureTargets(targetList); + + // Search through View-tree + View rootView = getView(); + Point point = new Point(); + Rect rect = new Rect(0, 0, rootView.getWidth(), rootView.getHeight()); + getChildVisibleRect(rootView, rect, point); + rootView.dispatchScrollCaptureSearch(rect, point, targetList); + + // No-op path. Scroll capture not offered for this window. + if (targetList.isEmpty()) { + dispatchScrollCaptureSearchResult(controller, null); + return; + } + + // Request scrollBounds from each of the targets. + // Continues with the consumer once all responses are consumed, or the timeout expires. + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetList); + resolver.start(mHandler, 1000, + (selected) -> dispatchScrollCaptureSearchResult(controller, selected)); + } + + /** Called by {@link #handleScrollCaptureRequest} when a result is returned */ + private void dispatchScrollCaptureSearchResult( + @NonNull IScrollCaptureController controller, + @Nullable ScrollCaptureTarget selectedTarget) { + + // If timeout or no eligible targets found. + if (selectedTarget == null) { + try { + if (DEBUG_SCROLL_CAPTURE) { + Log.d(TAG, "scrollCaptureSearch returned no targets available."); + } + controller.onClientUnavailable(); + } catch (RemoteException e) { + if (DEBUG_SCROLL_CAPTURE) { + Log.w(TAG, "Failed to notify controller of scroll capture search result.", e); + } + } + return; + } + + // Create a client instance and return it to the caller + mScrollCaptureClient = new ScrollCaptureClient(selectedTarget, controller); + try { + if (DEBUG_SCROLL_CAPTURE) { + Log.d(TAG, "scrollCaptureSearch returning client: " + getScrollCaptureClient()); + } + controller.onClientConnected( + mScrollCaptureClient, + selectedTarget.getScrollBounds(), + selectedTarget.getPositionInWindow()); + } catch (RemoteException e) { + if (DEBUG_SCROLL_CAPTURE) { + Log.w(TAG, "Failed to notify controller of scroll capture search result.", e); + } + mScrollCaptureClient.disconnect(); + mScrollCaptureClient = null; + } + } private void reportNextDraw() { if (mReportNextDraw == false) { @@ -9091,6 +9232,13 @@ public final class ViewRootImpl implements ViewParent, } } + @Override + public void requestScrollCapture(IScrollCaptureController controller) { + final ViewRootImpl viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchScrollCaptureRequest(controller); + } + } } public static final class CalledFromWrongThreadException extends AndroidRuntimeException { diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index ae9afabad533..b1536484b515 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -2535,6 +2535,33 @@ public abstract class Window { return Collections.emptyList(); } + /** + * System request to begin scroll capture. + * + * @param controller the controller to receive responses + * @hide + */ + public void requestScrollCapture(IScrollCaptureController controller) { + } + + /** + * Registers a {@link ScrollCaptureCallback} with the root of this window. + * + * @param callback the callback to add + * @hide + */ + public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { + } + + /** + * Unregisters a {@link ScrollCaptureCallback} previously registered with this window. + * + * @param callback the callback to remove + * @hide + */ + public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { + } + /** @hide */ public void setTheme(int resId) { } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index cc380f32297e..e4dbd63765b6 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -514,33 +514,24 @@ public interface WindowManager extends ViewManager { int TAKE_SCREENSHOT_PROVIDED_IMAGE = 3; /** - * Parcel key for the screen shot bitmap sent with messages of type - * {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}, type {@link android.graphics.Bitmap} - * @hide - */ - String PARCEL_KEY_SCREENSHOT_BITMAP = "screenshot_screen_bitmap"; - - /** - * Parcel key for the screen bounds of the image sent with messages of type - * [@link {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}], type {@link Rect} in screen coordinates. - * @hide - */ - String PARCEL_KEY_SCREENSHOT_BOUNDS = "screenshot_screen_bounds"; - - /** - * Parcel key for the task id of the task that the screen shot was taken of, sent with messages - * of type [@link {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}], type int. - * @hide - */ - String PARCEL_KEY_SCREENSHOT_TASK_ID = "screenshot_task_id"; - - /** - * Parcel key for the visible insets of the image sent with messages of type - * [@link {@link #TAKE_SCREENSHOT_PROVIDED_IMAGE}], type {@link android.graphics.Insets} in - * screen coordinates. + * Enum listing the possible sources from which a screenshot was originated. Used for logging. + * * @hide */ - String PARCEL_KEY_SCREENSHOT_INSETS = "screenshot_insets"; + @IntDef({ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS, + ScreenshotSource.SCREENSHOT_KEY_CHORD, + ScreenshotSource.SCREENSHOT_KEY_OTHER, + ScreenshotSource.SCREENSHOT_OVERVIEW, + ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS, + ScreenshotSource.SCREENSHOT_OTHER}) + @interface ScreenshotSource { + int SCREENSHOT_GLOBAL_ACTIONS = 0; + int SCREENSHOT_KEY_CHORD = 1; + int SCREENSHOT_KEY_OTHER = 2; + int SCREENSHOT_OVERVIEW = 3; + int SCREENSHOT_ACCESSIBILITY_ACTIONS = 4; + int SCREENSHOT_OTHER = 5; + } /** * @hide diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index ec5130143086..397bce44b25e 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -24,7 +24,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.Log; import android.util.MergedConfiguration; -import android.view.IWindowSession; import java.util.HashMap; diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 7f6c0d2077f1..7016c5cf0de6 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -1213,7 +1213,7 @@ public class RemoteViews implements Parcelable, Filter { BitmapReflectionAction(Parcel in) { viewId = in.readInt(); - methodName = in.readString(); + methodName = in.readString8(); bitmapId = in.readInt(); bitmap = mBitmapCache.getBitmapForId(bitmapId); } @@ -1221,7 +1221,7 @@ public class RemoteViews implements Parcelable, Filter { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(viewId); - dest.writeString(methodName); + dest.writeString8(methodName); dest.writeInt(bitmapId); } @@ -1282,7 +1282,7 @@ public class RemoteViews implements Parcelable, Filter { ReflectionAction(Parcel in) { this.viewId = in.readInt(); - this.methodName = in.readString(); + this.methodName = in.readString8(); this.type = in.readInt(); //noinspection ConstantIfStatement if (false) { @@ -1318,7 +1318,7 @@ public class RemoteViews implements Parcelable, Filter { this.value = (char)in.readInt(); break; case STRING: - this.value = in.readString(); + this.value = in.readString8(); break; case CHAR_SEQUENCE: this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); @@ -1347,7 +1347,7 @@ public class RemoteViews implements Parcelable, Filter { public void writeToParcel(Parcel out, int flags) { out.writeInt(this.viewId); - out.writeString(this.methodName); + out.writeString8(this.methodName); out.writeInt(this.type); //noinspection ConstantIfStatement if (false) { @@ -1383,7 +1383,7 @@ public class RemoteViews implements Parcelable, Filter { out.writeInt((int)((Character)this.value).charValue()); break; case STRING: - out.writeString((String)this.value); + out.writeString8((String)this.value); break; case CHAR_SEQUENCE: TextUtils.writeToParcel((CharSequence)this.value, out, flags); diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index d851a099d0e1..970bab9bc53b 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -2611,11 +2611,12 @@ public class ChooserActivity extends ResolverActivity implements * does not match either the personal or work user handle. **/ private int getProfileForUser(UserHandle currentUserHandle) { - if (currentUserHandle == getPersonalProfileUserHandle()) { + if (currentUserHandle.equals(getPersonalProfileUserHandle())) { return PROFILE_PERSONAL; - } else if (currentUserHandle == getWorkProfileUserHandle()) { + } else if (currentUserHandle.equals(getWorkProfileUserHandle())) { return PROFILE_WORK; } + Log.e(TAG, "User " + currentUserHandle + " does not belong to a personal or work profile."); return -1; } diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 36eecfb685e8..2c4892561ddd 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -50,6 +50,9 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * This is used in conjunction with @@ -74,11 +77,13 @@ public class IntentForwarderActivity extends Activity { private Injector mInjector; private MetricsLogger mMetricsLogger; + protected ExecutorService mExecutorService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mInjector = createInjector(); + mExecutorService = Executors.newSingleThreadExecutor(); Intent intentReceived = getIntent(); String className = intentReceived.getComponent().getClassName(); @@ -118,30 +123,9 @@ public class IntentForwarderActivity extends Activity { mInjector.getIPackageManager(), getContentResolver()); if (newIntent != null) { newIntent.prepareToLeaveUser(callingUserId); - - final ResolveInfo ri = mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, - targetUserId); - try { - startActivityAsCaller(newIntent, null, null, false, targetUserId); - } catch (RuntimeException e) { - int launchedFromUid = -1; - String launchedFromPackage = "?"; - try { - launchedFromUid = ActivityTaskManager.getService().getLaunchedFromUid( - getActivityToken()); - launchedFromPackage = ActivityTaskManager.getService().getLaunchedFromPackage( - getActivityToken()); - } catch (RemoteException ignored) { - } - - Slog.wtf(TAG, "Unable to launch as UID " + launchedFromUid + " package " - + launchedFromPackage + ", while running in " - + ActivityThread.currentProcessName(), e); - } - - if (shouldShowDisclosure(ri, intentReceived)) { - mInjector.showToast(userMessageId, Toast.LENGTH_LONG); - } + maybeShowDisclosureAsync(intentReceived, newIntent, targetUserId, userMessageId); + CompletableFuture.runAsync(() -> startActivityAsCaller( + newIntent, targetUserId), mExecutorService); } else { Slog.wtf(TAG, "the intent: " + intentReceived + " cannot be forwarded from user " + callingUserId + " to user " + targetUserId); @@ -149,6 +133,44 @@ public class IntentForwarderActivity extends Activity { finish(); } + private void maybeShowDisclosureAsync( + Intent intentReceived, Intent newIntent, int userId, int messageId) { + final CompletableFuture<ResolveInfo> resolveInfoFuture = + mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, userId); + resolveInfoFuture.thenAcceptAsync(ri -> { + if (shouldShowDisclosure(ri, intentReceived)) { + mInjector.showToast(messageId, Toast.LENGTH_LONG); + } + }, getApplicationContext().getMainExecutor()); + } + + private void startActivityAsCaller(Intent newIntent, int userId) { + try { + startActivityAsCaller( + newIntent, + /* options= */ null, + /* permissionToken= */ null, + /* ignoreTargetSecurity= */ false, + userId); + } catch (RuntimeException e) { + int launchedFromUid = -1; + String launchedFromPackage = "?"; + try { + launchedFromUid = ActivityTaskManager.getService().getLaunchedFromUid( + getActivityToken()); + launchedFromPackage = ActivityTaskManager.getService() + .getLaunchedFromPackage(getActivityToken()); + } catch (RemoteException ignored) { + } + + Slog.wtf(TAG, "Unable to launch as UID " + launchedFromUid + " package " + + launchedFromPackage + ", while running in " + + ActivityThread.currentProcessName(), e); + } finally { + mExecutorService.shutdown(); + } + } + private void launchChooserActivityWithCorrectTab(Intent intentReceived, String className) { // When showing the sharesheet, instead of forwarding to the other profile, // we launch the sharesheet in the current user and select the other tab. @@ -163,7 +185,7 @@ public class IntentForwarderActivity extends Activity { return; } sanitizeIntent(innerIntent); - startActivity(intentReceived); + startActivityAsCaller(intentReceived, null, null, false, getUserId()); finish(); } @@ -234,23 +256,7 @@ public class IntentForwarderActivity extends Activity { Intent intentToCheck = forwardIntent; if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) { - // The EXTRA_INITIAL_INTENTS may not be allowed to be forwarded. - if (forwardIntent.hasExtra(Intent.EXTRA_INITIAL_INTENTS)) { - Slog.wtf(TAG, "An chooser intent with extra initial intents cannot be forwarded to" - + " a different user"); - return null; - } - if (forwardIntent.hasExtra(Intent.EXTRA_REPLACEMENT_EXTRAS)) { - Slog.wtf(TAG, "A chooser intent with replacement extras cannot be forwarded to a" - + " different user"); - return null; - } - intentToCheck = forwardIntent.getParcelableExtra(Intent.EXTRA_INTENT); - if (intentToCheck == null) { - Slog.wtf(TAG, "Cannot forward a chooser intent with no extra " - + Intent.EXTRA_INTENT); - return null; - } + return null; } if (forwardIntent.getSelector() != null) { intentToCheck = forwardIntent.getSelector(); @@ -338,8 +344,11 @@ public class IntentForwarderActivity extends Activity { } @Override - public ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId) { - return getPackageManager().resolveActivityAsUser(intent, flags, userId); + @Nullable + public CompletableFuture<ResolveInfo> resolveActivityAsUser( + Intent intent, int flags, int userId) { + return CompletableFuture.supplyAsync( + () -> getPackageManager().resolveActivityAsUser(intent, flags, userId)); } @Override @@ -355,7 +364,7 @@ public class IntentForwarderActivity extends Activity { PackageManager getPackageManager(); - ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId); + CompletableFuture<ResolveInfo> resolveActivityAsUser(Intent intent, int flags, int userId); void showToast(@StringRes int messageId, int duration); } diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 43bd4a610910..64324756796a 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -6093,7 +6093,8 @@ public class BatteryStatsImpl extends BatteryStats { return array; } - public void noteNetworkInterfaceTypeLocked(String iface, int networkType) { + /** @hide */ + public void noteNetworkInterfaceType(String iface, int networkType) { if (TextUtils.isEmpty(iface)) return; synchronized (mModemNetworkLock) { diff --git a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java index ebfea450af88..56a6db95badc 100644 --- a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java +++ b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java @@ -46,6 +46,9 @@ public class GestureNavigationSettingsObserver extends ContentObserver { r.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT), false, this, UserHandle.USER_ALL); + r.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE), + false, this, UserHandle.USER_ALL); } public void unregister() { @@ -68,6 +71,11 @@ public class GestureNavigationSettingsObserver extends ContentObserver { return getSensitivity(userRes, Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT); } + public boolean areNavigationButtonForcedVisible() { + return Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) == 0; + } + private int getSensitivity(Resources userRes, String side) { final int inset = userRes.getDimensionPixelSize( com.android.internal.R.dimen.config_backGestureInset); diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index 25c114f4b7c1..23ba6530b072 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -79,6 +79,7 @@ import android.util.TypedValue; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.IRotationWatcher.Stub; +import android.view.IScrollCaptureController; import android.view.IWindowManager; import android.view.InputDevice; import android.view.InputEvent; @@ -89,6 +90,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; +import android.view.ScrollCaptureCallback; import android.view.SearchEvent; import android.view.SurfaceHolder.Callback2; import android.view.View; @@ -3916,4 +3918,35 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback { : null); } } + + /** + * System request to begin scroll capture. + * + * @param controller the controller to receive responses + * @hide + */ + @Override + public void requestScrollCapture(IScrollCaptureController controller) { + getViewRootImpl().dispatchScrollCaptureRequest(controller); + } + + /** + * Registers a handler providing scrolling capture support for window content. + * + * @param callback the callback to add + */ + @Override + public void addScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { + getViewRootImpl().addScrollCaptureCallback(callback); + } + + /** + * Unregisters the given {@link ScrollCaptureCallback}. + * + * @param callback the callback to remove + */ + @Override + public void removeScrollCaptureCallback(@NonNull ScrollCaptureCallback callback) { + getViewRootImpl().removeScrollCaptureCallback(callback); + } } diff --git a/core/java/com/android/internal/util/ScreenshotHelper.java b/core/java/com/android/internal/util/ScreenshotHelper.java index 7cff90bbf437..adadc5e20549 100644 --- a/core/java/com/android/internal/util/ScreenshotHelper.java +++ b/core/java/com/android/internal/util/ScreenshotHelper.java @@ -1,5 +1,7 @@ package com.android.internal.util; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ComponentName; @@ -10,11 +12,12 @@ import android.graphics.Bitmap; import android.graphics.Insets; import android.graphics.Rect; import android.net.Uri; -import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.Messenger; +import android.os.Parcel; +import android.os.Parcelable; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; @@ -23,6 +26,109 @@ import android.view.WindowManager; import java.util.function.Consumer; public class ScreenshotHelper { + + /** + * Describes a screenshot request (to make it easier to pass data through to the handler). + */ + public static class ScreenshotRequest implements Parcelable { + private int mSource; + private boolean mHasStatusBar; + private boolean mHasNavBar; + private Bitmap mBitmap; + private Rect mBoundsInScreen; + private Insets mInsets; + private int mTaskId; + + ScreenshotRequest(int source, boolean hasStatus, boolean hasNav) { + mSource = source; + mHasStatusBar = hasStatus; + mHasNavBar = hasNav; + } + + ScreenshotRequest( + int source, Bitmap bitmap, Rect boundsInScreen, Insets insets, int taskId) { + mSource = source; + mBitmap = bitmap; + mBoundsInScreen = boundsInScreen; + mInsets = insets; + mTaskId = taskId; + } + + ScreenshotRequest(Parcel in) { + mSource = in.readInt(); + mHasStatusBar = in.readBoolean(); + mHasNavBar = in.readBoolean(); + if (in.readInt() == 1) { + mBitmap = in.readParcelable(Bitmap.class.getClassLoader()); + mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader()); + mInsets = in.readParcelable(Insets.class.getClassLoader()); + mTaskId = in.readInt(); + } + } + + public int getSource() { + return mSource; + } + + public boolean getHasStatusBar() { + return mHasStatusBar; + } + + public boolean getHasNavBar() { + return mHasNavBar; + } + + public Bitmap getBitmap() { + return mBitmap; + } + + public Rect getBoundsInScreen() { + return mBoundsInScreen; + } + + public Insets getInsets() { + return mInsets; + } + + public int getTaskId() { + return mTaskId; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mSource); + dest.writeBoolean(mHasStatusBar); + dest.writeBoolean(mHasNavBar); + if (mBitmap == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + dest.writeParcelable(mBitmap, 0); + dest.writeParcelable(mBoundsInScreen, 0); + dest.writeParcelable(mInsets, 0); + dest.writeInt(mTaskId); + } + } + + public static final @NonNull Parcelable.Creator<ScreenshotRequest> CREATOR = + new Parcelable.Creator<ScreenshotRequest>() { + + @Override + public ScreenshotRequest createFromParcel(Parcel source) { + return new ScreenshotRequest(source); + } + + @Override + public ScreenshotRequest[] newArray(int size) { + return new ScreenshotRequest[size]; + } + }; + } private static final String TAG = "ScreenshotHelper"; // Time until we give up on the screenshot & show an error instead. @@ -36,8 +142,10 @@ public class ScreenshotHelper { mContext = context; } + + /** - * Request a screenshot be taken with a specific timeout. + * Request a screenshot be taken. * * Added to support reducing unit test duration; the method variant without a timeout argument * is recommended for general use. @@ -47,6 +155,32 @@ public class ScreenshotHelper { * or * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} * @param hasStatus {@code true} if the status bar is currently showing. {@code false} + * if not. + * @param hasNav {@code true} if the navigation bar is currently showing. {@code + * false} if not. + * @param source The source of the screenshot request. One of + * {SCREENSHOT_GLOBAL_ACTIONS, SCREENSHOT_KEY_CHORD, + * SCREENSHOT_OVERVIEW, SCREENSHOT_OTHER} + * @param handler A handler used in case the screenshot times out + * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the + * screenshot was taken. + */ + public void takeScreenshot(final int screenshotType, final boolean hasStatus, + final boolean hasNav, int source, @NonNull Handler handler, + @Nullable Consumer<Uri> completionConsumer) { + ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, hasStatus, hasNav); + takeScreenshot(screenshotType, SCREENSHOT_TIMEOUT_MS, handler, screenshotRequest, + completionConsumer); + } + + /** + * Request a screenshot be taken, with provided reason. + * + * @param screenshotType The type of screenshot, for example either + * {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN} + * or + * {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} + * @param hasStatus {@code true} if the status bar is currently showing. {@code false} * if * not. * @param hasNav {@code true} if the navigation bar is currently showing. {@code @@ -64,7 +198,7 @@ public class ScreenshotHelper { } /** - * Request a screenshot be taken. + * Request a screenshot be taken with a specific timeout. * * Added to support reducing unit test duration; the method variant without a timeout argument * is recommended for general use. @@ -89,9 +223,9 @@ public class ScreenshotHelper { public void takeScreenshot(final int screenshotType, final boolean hasStatus, final boolean hasNav, long timeoutMs, @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) { - takeScreenshot(screenshotType, hasStatus, hasNav, timeoutMs, handler, null, - completionConsumer - ); + ScreenshotRequest screenshotRequest = new ScreenshotRequest(SCREENSHOT_OTHER, hasStatus, + hasNav); + takeScreenshot(screenshotType, timeoutMs, handler, screenshotRequest, completionConsumer); } /** @@ -106,23 +240,16 @@ public class ScreenshotHelper { * screenshot was taken. */ public void provideScreenshot(@NonNull Bitmap screenshot, @NonNull Rect boundsInScreen, - @NonNull Insets insets, int taskId, @NonNull Handler handler, - @Nullable Consumer<Uri> completionConsumer) { - Bundle imageBundle = new Bundle(); - imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_BITMAP, screenshot); - imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_BOUNDS, boundsInScreen); - imageBundle.putParcelable(WindowManager.PARCEL_KEY_SCREENSHOT_INSETS, insets); - imageBundle.putInt(WindowManager.PARCEL_KEY_SCREENSHOT_TASK_ID, taskId); - - takeScreenshot( - WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, - false, false, // ignored when image bundle is set - SCREENSHOT_TIMEOUT_MS, handler, imageBundle, completionConsumer); + @NonNull Insets insets, int taskId, int source, + @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) { + ScreenshotRequest screenshotRequest = + new ScreenshotRequest(source, screenshot, boundsInScreen, insets, taskId); + takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_TIMEOUT_MS, + handler, screenshotRequest, completionConsumer); } - private void takeScreenshot(final int screenshotType, final boolean hasStatus, - final boolean hasNav, long timeoutMs, @NonNull Handler handler, - @Nullable Bundle providedImage, @Nullable Consumer<Uri> completionConsumer) { + private void takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler, + ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer) { synchronized (mScreenshotLock) { if (mScreenshotConnection != null) { return; @@ -157,7 +284,7 @@ public class ScreenshotHelper { return; } Messenger messenger = new Messenger(service); - Message msg = Message.obtain(null, screenshotType); + Message msg = Message.obtain(null, screenshotType, screenshotRequest); final ServiceConnection myConn = this; Handler h = new Handler(handler.getLooper()) { @Override @@ -175,12 +302,6 @@ public class ScreenshotHelper { } }; msg.replyTo = new Messenger(h); - msg.arg1 = hasStatus ? 1 : 0; - msg.arg2 = hasNav ? 1 : 0; - - if (screenshotType == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { - msg.setData(providedImage); - } try { messenger.send(msg); diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index 47f094f292f2..7f3eb4515654 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -26,6 +26,7 @@ import android.os.RemoteException; import android.util.MergedConfiguration; import android.view.DisplayCutout; import android.view.DragEvent; +import android.view.IScrollCaptureController; import android.view.IWindow; import android.view.IWindowSession; import android.view.InsetsSourceControl; @@ -169,4 +170,13 @@ public class BaseIWindow extends IWindow.Stub { @Override public void dispatchPointerCaptureChanged(boolean hasCapture) { } + + @Override + public void requestScrollCapture(IScrollCaptureController controller) { + try { + controller.onClientUnavailable(); + } catch (RemoteException ex) { + // ignore + } + } } diff --git a/core/java/com/android/internal/view/ScrollCaptureInternal.java b/core/java/com/android/internal/view/ScrollCaptureInternal.java new file mode 100644 index 000000000000..c589afdeaa1a --- /dev/null +++ b/core/java/com/android/internal/view/ScrollCaptureInternal.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2020 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.view; + +import android.annotation.Nullable; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.ScrollCaptureCallback; +import android.view.View; +import android.view.ViewGroup; + +/** + * Provides built-in framework level Scroll Capture support for standard scrolling Views. + */ +public class ScrollCaptureInternal { + private static final String TAG = "ScrollCaptureInternal"; + + private static final int UP = -1; + private static final int DOWN = 1; + + /** + * Not a ViewGroup, or cannot scroll according to View APIs. + */ + public static final int TYPE_FIXED = 0; + + /** + * Slides a single child view using mScrollX/mScrollY. + */ + public static final int TYPE_SCROLLING = 1; + + /** + * Slides child views through the viewport by translating their layout positions with {@link + * View#offsetTopAndBottom(int)}. Manages Child view lifecycle, creating as needed and + * binding views to data from an adapter. Views are reused whenever possible. + */ + public static final int TYPE_RECYCLING = 2; + + /** + * Performs tests on the given View and determines: + * 1. If scrolling is possible + * 2. What mechanisms are used for scrolling. + * <p> + * This needs to be fast and not alloc memory. It's called on everything in the tree not marked + * as excluded during scroll capture search. + */ + public static int detectScrollingType(View view) { + // Must be a ViewGroup + if (!(view instanceof ViewGroup)) { + return TYPE_FIXED; + } + // Confirm that it can scroll. + if (!(view.canScrollVertically(DOWN) || view.canScrollVertically(UP))) { + // Nothing to scroll here, move along. + return TYPE_FIXED; + } + // ScrollViews accept only a single child. + if (((ViewGroup) view).getChildCount() > 1) { + return TYPE_RECYCLING; + } + //Because recycling containers don't use scrollY, a non-zero value means Scroll view. + if (view.getScrollY() != 0) { + return TYPE_SCROLLING; + } + // Since scrollY cannot be negative, this means a Recycling view. + if (view.canScrollVertically(UP)) { + return TYPE_RECYCLING; + } + // canScrollVertically(UP) == false, getScrollY() == 0, getChildCount() == 1. + + // For Recycling containers, this should be a no-op (RecyclerView logs a warning) + view.scrollTo(view.getScrollX(), 1); + + // A scrolling container would have moved by 1px. + if (view.getScrollY() == 1) { + view.scrollTo(view.getScrollX(), 0); + return TYPE_SCROLLING; + } + return TYPE_RECYCLING; + } + + /** + * Creates a scroll capture callback for the given view if possible. + * + * @param view the view to capture + * @param localVisibleRect the visible area of the given view in local coordinates, as supplied + * by the view parent + * @param positionInWindow the offset of localVisibleRect within the window + * + * @return a new callback or null if the View isn't supported + */ + @Nullable + public ScrollCaptureCallback requestCallback(View view, Rect localVisibleRect, + Point positionInWindow) { + // Nothing to see here yet. + int i = detectScrollingType(view); + switch (i) { + case TYPE_SCROLLING: + return new ScrollCaptureViewSupport<>((ViewGroup) view, + new ScrollViewCaptureHelper()); + } + return null; + } +} diff --git a/core/java/com/android/internal/view/ScrollCaptureViewHelper.java b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java new file mode 100644 index 000000000000..9f100bd6440f --- /dev/null +++ b/core/java/com/android/internal/view/ScrollCaptureViewHelper.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 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.view; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.view.View; + +interface ScrollCaptureViewHelper<V extends View> { + int UP = -1; + int DOWN = 1; + + /** + * Verifies that the view is still visible and scrollable. If true is returned here, expect a + * call to {@link #onComputeScrollBounds(View)} to follow. + * + * @param view the view being captured + * @return true if the callback should respond to a request with scroll bounds + */ + default boolean onAcceptSession(@Nullable V view) { + return view != null && view.isVisibleToUser() + && (view.canScrollVertically(UP) || view.canScrollVertically(DOWN)); + } + + /** + * Given a scroll capture request for a view, adjust the provided rect to cover the scrollable + * content area. The default implementation returns the padded content area of {@code view}. + * + * @param view the view being captured + */ + default Rect onComputeScrollBounds(@Nullable V view) { + return new Rect(view.getPaddingLeft(), view.getPaddingTop(), + view.getWidth() - view.getPaddingRight(), + view.getHeight() - view.getPaddingBottom()); + } + /** + * Adjust the target for capture. + * <p> + * Do not touch anything that may change layout positions or sizes on screen. Anything else may + * be adjusted as long as it can be reversed in {@link #onPrepareForEnd(View)}. + * + * @param view the view being captured + * @param scrollBounds the bounds within {@code view} where content scrolls + */ + void onPrepareForStart(@NonNull V view, Rect scrollBounds); + + /** + * Map the request onto the screen. + * <p> + * Given a rect describing the area to capture, relative to scrollBounds, take actions + * necessary to bring the content within the rectangle into the visible area of the view if + * needed and return the resulting rectangle describing the position and bounds of the area + * which is visible. + * + * @param scrollBounds the area in which scrolling content moves, local to the {@code containing + * view} + * @param requestRect the area relative to {@code scrollBounds} which describes the location of + * content to capture for the request + * @return the visible area within scrollBounds of the requested rectangle, return {@code null} + * in the case of an unrecoverable error condition, to abort the capture process + */ + Rect onScrollRequested(@NonNull V view, Rect scrollBounds, Rect requestRect); + + /** + * Restore the target after capture. + * <p> + * Put back anything that was changed in {@link #onPrepareForStart(View, Rect)}. + * + * @param view the view being captured + */ + void onPrepareForEnd(@NonNull V view); +} diff --git a/core/java/com/android/internal/view/ScrollCaptureViewSupport.java b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java new file mode 100644 index 000000000000..4087eda944e0 --- /dev/null +++ b/core/java/com/android/internal/view/ScrollCaptureViewSupport.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2020 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.view; + +import android.graphics.HardwareRenderer; +import android.graphics.Matrix; +import android.graphics.RecordingCanvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.RenderNode; +import android.os.Handler; +import android.os.SystemClock; +import android.util.DisplayMetrics; +import android.view.ScrollCaptureCallback; +import android.view.ScrollCaptureSession; +import android.view.Surface; +import android.view.View; + +import java.lang.ref.WeakReference; +import java.util.function.Consumer; + +/** + * Provides a ScrollCaptureCallback implementation for to handle arbitrary View-based scrolling + * containers. + * <p> + * To use this class, supply the target view and an implementation of {@ScrollCaptureViewHelper} + * to the callback. + * + * @param <V> the specific View subclass handled + * @hide + */ +public class ScrollCaptureViewSupport<V extends View> implements ScrollCaptureCallback { + + private final WeakReference<V> mWeakView; + private final ScrollCaptureViewHelper<V> mViewHelper; + private ViewRenderer mRenderer; + private Handler mUiHandler; + private boolean mStarted; + private boolean mEnded; + + static <V extends View> ScrollCaptureCallback createCallback(V view, + ScrollCaptureViewHelper<V> impl) { + return new ScrollCaptureViewSupport<>(view, impl); + } + + ScrollCaptureViewSupport(V containingView, ScrollCaptureViewHelper<V> viewHelper) { + mWeakView = new WeakReference<>(containingView); + mRenderer = new ViewRenderer(); + mUiHandler = containingView.getHandler(); + mViewHelper = viewHelper; + } + + // Base implementation of ScrollCaptureCallback + + @Override + public final void onScrollCaptureSearch(Consumer<Rect> onReady) { + V view = mWeakView.get(); + mStarted = false; + mEnded = false; + + if (view != null && view.isVisibleToUser() && mViewHelper.onAcceptSession(view)) { + onReady.accept(mViewHelper.onComputeScrollBounds(view)); + return; + } + onReady.accept(null); + } + + @Override + public final void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) { + V view = mWeakView.get(); + mEnded = false; + mStarted = true; + + // Note: If somehow the view is already gone or detached, the first call to + // {@code onScrollCaptureImageRequest} will return an error and request the session to + // end. + if (view != null && view.isVisibleToUser()) { + mRenderer.setSurface(session.getSurface()); + mViewHelper.onPrepareForStart(view, session.getScrollBounds()); + } + onReady.run(); + } + + @Override + public final void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect requestRect) { + V view = mWeakView.get(); + if (view == null || !view.isVisibleToUser()) { + // Signal to the controller that we have a problem and can't continue. + session.notifyBufferSent(0, null); + return; + } + Rect captureArea = mViewHelper.onScrollRequested(view, session.getScrollBounds(), + requestRect); + mRenderer.renderFrame(view, captureArea, mUiHandler, + () -> session.notifyBufferSent(0, captureArea)); + } + + @Override + public final void onScrollCaptureEnd(Runnable onReady) { + V view = mWeakView.get(); + if (mStarted && !mEnded) { + mViewHelper.onPrepareForEnd(view); + /* empty */ + mEnded = true; + mRenderer.trimMemory(); + mRenderer.setSurface(null); + } + onReady.run(); + } + + /** + * Internal helper class which assists in rendering sections of the view hierarchy relative to a + * given view. Used by framework implementations of ScrollCaptureHandler to render and dispatch + * image requests. + */ + static final class ViewRenderer { + // alpha, "reasonable default" from Javadoc + private static final float AMBIENT_SHADOW_ALPHA = 0.039f; + private static final float SPOT_SHADOW_ALPHA = 0.039f; + + // Default values: + // lightX = (screen.width() / 2) - windowLeft + // lightY = 0 - windowTop + // lightZ = 600dp + // lightRadius = 800dp + private static final float LIGHT_Z_DP = 400; + private static final float LIGHT_RADIUS_DP = 800; + private static final String TAG = "ViewRenderer"; + + private HardwareRenderer mRenderer; + private RenderNode mRootRenderNode; + private final RectF mTempRectF = new RectF(); + private final Rect mSourceRect = new Rect(); + private final Rect mTempRect = new Rect(); + private final Matrix mTempMatrix = new Matrix(); + private final int[] mTempLocation = new int[2]; + private long mLastRenderedSourceDrawingId = -1; + + + ViewRenderer() { + mRenderer = new HardwareRenderer(); + mRootRenderNode = new RenderNode("ScrollCaptureRoot"); + mRenderer.setContentRoot(mRootRenderNode); + + // TODO: Figure out a way to flip this on when we are sure the source window is opaque + mRenderer.setOpaque(false); + } + + public void setSurface(Surface surface) { + mRenderer.setSurface(surface); + } + + /** + * Cache invalidation check. If the source view is the same as the previous call (which is + * mostly always the case, then we can skip setting up lighting on each call (for now) + * + * @return true if the view changed, false if the view was previously rendered by this class + */ + private boolean updateForView(View source) { + if (mLastRenderedSourceDrawingId == source.getUniqueDrawingId()) { + return false; + } + mLastRenderedSourceDrawingId = source.getUniqueDrawingId(); + return true; + } + + // TODO: may need to adjust lightY based on the virtual canvas position to get + // consistent shadow positions across the whole capture. Or possibly just + // pull lightZ way back to make shadows more uniform. + private void setupLighting(View mSource) { + mLastRenderedSourceDrawingId = mSource.getUniqueDrawingId(); + DisplayMetrics metrics = mSource.getResources().getDisplayMetrics(); + mSource.getLocationOnScreen(mTempLocation); + final float lightX = metrics.widthPixels / 2f - mTempLocation[0]; + final float lightY = metrics.heightPixels - mTempLocation[1]; + final int lightZ = (int) (LIGHT_Z_DP * metrics.density); + final int lightRadius = (int) (LIGHT_RADIUS_DP * metrics.density); + + // Enable shadows for elevation/Z + mRenderer.setLightSourceGeometry(lightX, lightY, lightZ, lightRadius); + mRenderer.setLightSourceAlpha(AMBIENT_SHADOW_ALPHA, SPOT_SHADOW_ALPHA); + + } + + public void renderFrame(View localReference, Rect sourceRect, Handler handler, + Runnable onFrameCommitted) { + if (updateForView(localReference)) { + setupLighting(localReference); + } + buildRootDisplayList(localReference, sourceRect); + HardwareRenderer.FrameRenderRequest request = mRenderer.createRenderRequest(); + request.setVsyncTime(SystemClock.elapsedRealtimeNanos()); + request.setFrameCommitCallback(handler::post, onFrameCommitted); + request.setWaitForPresent(true); + request.syncAndDraw(); + } + + public void trimMemory() { + mRenderer.clearContent(); + } + + public void destroy() { + mRenderer.destroy(); + } + + private void transformToRoot(View local, Rect localRect, Rect outRect) { + mTempMatrix.reset(); + local.transformMatrixToGlobal(mTempMatrix); + mTempRectF.set(localRect); + mTempMatrix.mapRect(mTempRectF); + mTempRectF.round(outRect); + } + + private void buildRootDisplayList(View source, Rect localSourceRect) { + final View captureSource = source.getRootView(); + transformToRoot(source, localSourceRect, mTempRect); + mRootRenderNode.setPosition(0, 0, mTempRect.width(), mTempRect.height()); + RecordingCanvas canvas = mRootRenderNode.beginRecording(mTempRect.width(), + mTempRect.height()); + canvas.translate(-mTempRect.left, -mTempRect.top); + canvas.drawRenderNode(captureSource.updateDisplayListIfDirty()); + mRootRenderNode.endRecording(); + } + } +} diff --git a/core/java/com/android/internal/view/ScrollViewCaptureHelper.java b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java new file mode 100644 index 000000000000..12bd461f810b --- /dev/null +++ b/core/java/com/android/internal/view/ScrollViewCaptureHelper.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2020 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.view; + +import android.annotation.NonNull; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +/** + * ScrollCapture for ScrollView and <i>ScrollView-like</i> ViewGroups. + * <p> + * Requirements for proper operation: + * <ul> + * <li>contains at most 1 child. + * <li>scrolls to absolute positions with {@link View#scrollTo(int, int)}. + * <li>has a finite, known content height and scrolling range + * <li>correctly implements {@link View#canScrollVertically(int)} + * <li>correctly implements {@link ViewParent#requestChildRectangleOnScreen(View, + * Rect, boolean)} + * </ul> + */ +public class ScrollViewCaptureHelper implements ScrollCaptureViewHelper<ViewGroup> { + private int mStartScrollY; + private boolean mScrollBarEnabled; + private int mOverScrollMode; + + /** @see ScrollCaptureViewHelper#onPrepareForStart(View, Rect) */ + public void onPrepareForStart(@NonNull ViewGroup view, Rect scrollBounds) { + mStartScrollY = view.getScrollY(); + mOverScrollMode = view.getOverScrollMode(); + if (mOverScrollMode != View.OVER_SCROLL_NEVER) { + view.setOverScrollMode(View.OVER_SCROLL_NEVER); + } + mScrollBarEnabled = view.isVerticalScrollBarEnabled(); + if (mScrollBarEnabled) { + view.setVerticalScrollBarEnabled(false); + } + } + + /** @see ScrollCaptureViewHelper#onScrollRequested(View, Rect, Rect) */ + public Rect onScrollRequested(@NonNull ViewGroup view, Rect scrollBounds, Rect requestRect) { + final View contentView = view.getChildAt(0); // returns null, does not throw IOOBE + if (contentView == null) { + return null; + } + /* + +---------+ <----+ Content [25,25 - 275,1025] (w=250,h=1000) + | | + ...|.........|... startScrollY=100 + | | + +--+---------+---+ <--+ Container View [0,0 - 300,300] (scrollY=200) + | . . | + --- | . +-----+ <------+ Scroll Bounds [50,50 - 250,250] (200x200) + ^ | . | | . | (Local to Container View, fixed/un-scrolled) + | | . | | . | + | | . | | . | + | | . +-----+ . | + | | . . | + | +--+---------+---+ + | | | + -+- | +-----+ | + | |#####| | <--+ Requested Bounds [0,300 - 200,400] (200x100) + | +-----+ | (Local to Scroll Bounds, fixed/un-scrolled) + | | + +---------+ + + Container View (ScrollView) [0,0 - 300,300] (scrollY = 200) + \__ Content [25,25 - 275,1025] (250x1000) (contentView) + \__ Scroll Bounds[50,50 - 250,250] (w=200,h=200) + \__ Requested Bounds[0,300 - 200,400] (200x100) + */ + + // 0) adjust the requestRect to account for scroll change since start + // + // Scroll Bounds[50,50 - 250,250] (w=200,h=200) + // \__ Requested Bounds[0,200 - 200,300] (200x100) + + // (y-100) (scrollY - mStartScrollY) + int scrollDelta = view.getScrollY() - mStartScrollY; + + // 1) Translate request rect to make it relative to container view + // + // Container View [0,0 - 300,300] (scrollY=200) + // \__ Requested Bounds[50,250 - 250,350] (w=250, h=100) + + // (x+50,y+50) + Rect requestedContainerBounds = new Rect(requestRect); + requestedContainerBounds.offset(0, -scrollDelta); + requestedContainerBounds.offset(scrollBounds.left, scrollBounds.top); + + // 2) Translate from container to contentView relative (applying container scrollY) + // + // Container View [0,0 - 300,300] (scrollY=200) + // \__ Content [25,25 - 275,1025] (250x1000) (contentView) + // \__ Requested Bounds[25,425 - 200,525] (w=250, h=100) + + // (x-25,y+175) (scrollY - content.top) + Rect requestedContentBounds = new Rect(requestedContainerBounds); + requestedContentBounds.offset( + view.getScrollX() - contentView.getLeft(), + view.getScrollY() - contentView.getTop()); + + + + // requestRect is now local to contentView as requestedContentBounds + // contentView (and each parent in turn if possible) will be scrolled + // (if necessary) to make all of requestedContent visible, (if possible!) + contentView.requestRectangleOnScreen(new Rect(requestedContentBounds), true); + + // update new offset between starting and current scroll position + scrollDelta = view.getScrollY() - mStartScrollY; + + + // TODO: adjust to avoid occlusions/minimize scroll changes + + Point offset = new Point(); + final Rect capturedRect = new Rect(requestedContentBounds); // empty + if (!view.getChildVisibleRect(contentView, capturedRect, offset)) { + capturedRect.setEmpty(); + return capturedRect; + } + // Transform back from global to content-view local + capturedRect.offset(-offset.x, -offset.y); + + // Then back to container view + capturedRect.offset( + contentView.getLeft() - view.getScrollX(), + contentView.getTop() - view.getScrollY()); + + + // And back to relative to scrollBounds + capturedRect.offset(-scrollBounds.left, -scrollBounds.top); + + // Apply scrollDelta again to return to make capturedRect relative to scrollBounds at + // the scroll position at start of capture. + capturedRect.offset(0, scrollDelta); + return capturedRect; + } + + /** @see ScrollCaptureViewHelper#onPrepareForEnd(View) */ + public void onPrepareForEnd(@NonNull ViewGroup view) { + view.scrollTo(0, mStartScrollY); + if (mOverScrollMode != View.OVER_SCROLL_NEVER) { + view.setOverScrollMode(mOverScrollMode); + } + if (mScrollBarEnabled) { + view.setVerticalScrollBarEnabled(true); + } + } +} diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index ba7aef7c208e..b51d4f509f38 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -1244,12 +1244,11 @@ void AndroidRuntime::exit(int code) { if (mExitWithoutCleanup) { ALOGI("VM exiting with result code %d, cleanup skipped.", code); - ::_exit(code); } else { ALOGI("VM exiting with result code %d.", code); onExit(code); - ::exit(code); } + ::_exit(code); } void AndroidRuntime::onVmCreated(JNIEnv* env) diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp index 483b455965f7..e7a2fb428b50 100644 --- a/core/jni/android_os_Parcel.cpp +++ b/core/jni/android_os_Parcel.cpp @@ -273,7 +273,28 @@ static void android_os_Parcel_writeDouble(JNIEnv* env, jclass clazz, jlong nativ } } -static void android_os_Parcel_writeString(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val) +static void android_os_Parcel_writeString8(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val) +{ + Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); + if (parcel != NULL) { + status_t err = NO_MEMORY; + if (val) { + const size_t len = env->GetStringUTFLength(val); + const char* str = env->GetStringUTFChars(val, 0); + if (str) { + err = parcel->writeString8(str, len); + env->ReleaseStringUTFChars(val, str); + } + } else { + err = parcel->writeString8(NULL, 0); + } + if (err != NO_ERROR) { + signalExceptionForError(env, clazz, err); + } + } +} + +static void android_os_Parcel_writeString16(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -444,7 +465,21 @@ static jdouble android_os_Parcel_readDouble(jlong nativePtr) return 0; } -static jstring android_os_Parcel_readString(JNIEnv* env, jclass clazz, jlong nativePtr) +static jstring android_os_Parcel_readString8(JNIEnv* env, jclass clazz, jlong nativePtr) +{ + Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); + if (parcel != NULL) { + size_t len; + const char* str = parcel->readString8Inplace(&len); + if (str) { + return env->NewStringUTF(str); + } + return NULL; + } + return NULL; +} + +static jstring android_os_Parcel_readString16(JNIEnv* env, jclass clazz, jlong nativePtr) { Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); if (parcel != NULL) { @@ -722,7 +757,9 @@ static const JNINativeMethod gParcelMethods[] = { // @FastNative {"nativeWriteDouble", "(JD)V", (void*)android_os_Parcel_writeDouble}, // @FastNative - {"nativeWriteString", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString}, + {"nativeWriteString8", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString8}, + // @FastNative + {"nativeWriteString16", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeString16}, // @FastNative {"nativeWriteStrongBinder", "(JLandroid/os/IBinder;)V", (void*)android_os_Parcel_writeStrongBinder}, // @FastNative @@ -740,7 +777,9 @@ static const JNINativeMethod gParcelMethods[] = { // @CriticalNative {"nativeReadDouble", "(J)D", (void*)android_os_Parcel_readDouble}, // @FastNative - {"nativeReadString", "(J)Ljava/lang/String;", (void*)android_os_Parcel_readString}, + {"nativeReadString8", "(J)Ljava/lang/String;", (void*)android_os_Parcel_readString8}, + // @FastNative + {"nativeReadString16", "(J)Ljava/lang/String;", (void*)android_os_Parcel_readString16}, // @FastNative {"nativeReadStrongBinder", "(J)Landroid/os/IBinder;", (void*)android_os_Parcel_readStrongBinder}, // @FastNative diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 924dc4b3a051..21985f0bb4bb 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -119,6 +119,7 @@ typedef const std::function<void(std::string)>& fail_fn_t; static pid_t gSystemServerPid = 0; +static constexpr const char* kVoldAppDataIsolation = "persist.sys.vold_app_data_isolation_enabled"; static constexpr const char* kPropFuse = "persist.sys.fuse"; static const char kZygoteClassName[] = "com/android/internal/os/Zygote"; static jclass gZygoteClass; @@ -831,6 +832,7 @@ static void MountEmulatedStorage(uid_t uid, jint mount_mode, multiuser_get_uid(user_id, AID_EVERYBODY), fail_fn); bool isFuse = GetBoolProperty(kPropFuse, false); + bool isAppDataIsolationEnabled = GetBoolProperty(kVoldAppDataIsolation, false); if (isFuse) { if (mount_mode == MOUNT_EXTERNAL_PASS_THROUGH) { @@ -840,6 +842,9 @@ static void MountEmulatedStorage(uid_t uid, jint mount_mode, } else if (mount_mode == MOUNT_EXTERNAL_INSTALLER) { const std::string installer_source = StringPrintf("/mnt/installer/%d", user_id); BindMount(installer_source, "/storage", fail_fn); + } else if (isAppDataIsolationEnabled && mount_mode == MOUNT_EXTERNAL_ANDROID_WRITABLE) { + const std::string writable_source = StringPrintf("/mnt/androidwritable/%d", user_id); + BindMount(writable_source, "/storage", fail_fn); } else { BindMount(user_source, "/storage", fail_fn); } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 6e8b9de2e732..ee25ac27b25c 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1111,12 +1111,13 @@ grants your app this permission. If you don't need this permission, be sure your <a href="{@docRoot}guide/topics/manifest/uses-sdk-element.html#target">{@code targetSdkVersion}</a> is 4 or higher. - <p>Protection level: normal + <p>Protection level: dangerous --> <permission android:name="android.permission.READ_PHONE_STATE" + android:permissionGroup="android.permission-group.UNDEFINED" android:label="@string/permlab_readPhoneState" android:description="@string/permdesc_readPhoneState" - android:protectionLevel="normal" /> + android:protectionLevel="dangerous" /> <!-- Allows read access to the device's phone number(s). This is a subset of the capabilities granted by {@link #READ_PHONE_STATE} but is exposed to instant applications. diff --git a/core/res/res/layout/resolver_empty_states.xml b/core/res/res/layout/resolver_empty_states.xml index 5890bed11b07..25615d2dc304 100644 --- a/core/res/res/layout/resolver_empty_states.xml +++ b/core/res/res/layout/resolver_empty_states.xml @@ -43,6 +43,7 @@ android:fontFamily="@string/config_headlineFontFamilyMedium" android:textColor="@color/resolver_empty_state_text" android:textSize="14sp" + android:gravity="center_horizontal" android:layout_centerHorizontal="true" /> <TextView android:id="@+id/resolver_empty_state_subtitle" diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 4065a6c83f8f..a8d1605c6fdd 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2527,6 +2527,21 @@ <flag name="noExcludeDescendants" value="0x8" /> </attr> + <!-- Hints the Android System whether the this View should be considered a scroll capture target. --> + <attr name="scrollCaptureHint"> + <!-- Let the Android System determine if the view can be a scroll capture target. --> + <flag name="auto" value="0" /> + <!-- Hint the Android System that this view is a likely target. If capable, it will + be ranked above other views without this flag. --> + <flag name="include" value="0x1" /> + <!-- Hint the Android System that this view should never be considered a scroll capture + target. --> + <flag name="exclude" value="0x2" /> + <!-- Hint the Android System that this view's children should not be examined and should + be excluded as a scroll capture target. --> + <flag name="excludeDescendants" value="0x4" /> + </attr> + <!-- Boolean that controls whether a view can take focus while in touch mode. If this is true for a view, that view can gain focus when clicked on, and can keep focus if another view is clicked on that doesn't have this attribute set to true. --> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 67d20da03925..fb887c338fc4 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -3020,6 +3020,8 @@ <public name="preserveLegacyExternalStorage" /> <public name="mimeGroup" /> <public name="gwpAsanMode" /> + <!-- @hide --> + <public name="scrollCaptureHint" /> </public-group> <public-group type="drawable" first-id="0x010800b5"> diff --git a/core/res/res/xml/default_zen_mode_config.xml b/core/res/res/xml/default_zen_mode_config.xml index 9110661536e2..873b9ebe4f1a 100644 --- a/core/res/res/xml/default_zen_mode_config.xml +++ b/core/res/res/xml/default_zen_mode_config.xml @@ -20,8 +20,8 @@ <!-- Default configuration for zen mode. See android.service.notification.ZenModeConfig. --> <zen version="9"> <allow alarms="true" media="true" system="false" calls="true" callsFrom="2" messages="false" - reminders="false" events="false" repeatCallers="true" conversations="true" - conversationsFrom="2"/> + reminders="false" events="false" repeatCallers="true" convos="false" + convosFrom="3"/> <automatic ruleId="EVENTS_DEFAULT_RULE" enabled="false" snoozing="false" name="Event" zen="1" component="android/com.android.server.notification.EventConditionProvider" conditionId="condition://android/event?userId=-10000&calendar=&reply=1"/> diff --git a/core/tests/benchmarks/src/android/os/ParcelStringBenchmark.java b/core/tests/benchmarks/src/android/os/ParcelStringBenchmark.java new file mode 100644 index 000000000000..daa90c28e0c4 --- /dev/null +++ b/core/tests/benchmarks/src/android/os/ParcelStringBenchmark.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import com.google.caliper.AfterExperiment; +import com.google.caliper.BeforeExperiment; +import com.google.caliper.Param; + +public class ParcelStringBenchmark { + + @Param({"com.example.typical_package_name", "從不喜歡孤單一個 - 蘇永康/吳雨霏"}) + String mValue; + + private Parcel mParcel; + + @BeforeExperiment + protected void setUp() { + mParcel = Parcel.obtain(); + } + + @AfterExperiment + protected void tearDown() { + mParcel.recycle(); + mParcel = null; + } + + public void timeWriteString8(int reps) { + for (int i = 0; i < reps; i++) { + mParcel.setDataPosition(0); + mParcel.writeString8(mValue); + } + } + + public void timeReadString8(int reps) { + mParcel.writeString8(mValue); + + for (int i = 0; i < reps; i++) { + mParcel.setDataPosition(0); + mParcel.readString8(); + } + } + + public void timeWriteString16(int reps) { + for (int i = 0; i < reps; i++) { + mParcel.setDataPosition(0); + mParcel.writeString16(mValue); + } + } + + public void timeReadString16(int reps) { + mParcel.writeString16(mValue); + + for (int i = 0; i < reps; i++) { + mParcel.setDataPosition(0); + mParcel.readString16(); + } + } +} diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index 46873b9eb70b..dcb3e2f23da8 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -87,4 +87,27 @@ public class ParcelTest { p.recycle(); } + + /** + * Verify that writing/reading UTF-8 and UTF-16 strings works well. + */ + @Test + public void testStrings() { + final String[] strings = { + null, "", "abc\0def", "com.example.typical_package_name", + "從不喜歡孤單一個 - 蘇永康/吳雨霏", "example" + }; + + final Parcel p = Parcel.obtain(); + for (String string : strings) { + p.writeString8(string); + p.writeString16(string); + } + + p.setDataPosition(0); + for (String string : strings) { + assertEquals(string, p.readString8()); + assertEquals(string, p.readString16()); + } + } } diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java index ea778fd6710a..a354f1d8109f 100644 --- a/core/tests/coretests/src/android/os/VibrationEffectTest.java +++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java @@ -22,9 +22,12 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.content.ContentInterface; +import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.net.Uri; @@ -37,6 +40,8 @@ import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class VibrationEffectTest { + private static final float SCALE_TOLERANCE = 1e-2f; + private static final String RINGTONE_URI_1 = "content://test/system/ringtone_1"; private static final String RINGTONE_URI_2 = "content://test/system/ringtone_2"; private static final String RINGTONE_URI_3 = "content://test/system/ringtone_3"; @@ -54,6 +59,12 @@ public class VibrationEffectTest { VibrationEffect.createOneShot(TEST_TIMING, VibrationEffect.DEFAULT_AMPLITUDE); private static final VibrationEffect TEST_WAVEFORM = VibrationEffect.createWaveform(TEST_TIMINGS, TEST_AMPLITUDES, -1); + private static final VibrationEffect TEST_COMPOSED = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 10) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0, 100) + .compose(); @Test public void getRingtones_noPrebakedRingtones() { @@ -123,8 +134,14 @@ public class VibrationEffectTest { @Test public void testScaleWaveform() { - VibrationEffect.Waveform scaled = - ((VibrationEffect.Waveform) TEST_WAVEFORM).scale(1.1f, 200); + VibrationEffect.Waveform initial = (VibrationEffect.Waveform) TEST_WAVEFORM; + + VibrationEffect.Waveform copied = initial.scale(1f, 255); + assertEquals(255, copied.getAmplitudes()[0]); + assertEquals(0, copied.getAmplitudes()[1]); + assertEquals(-1, copied.getAmplitudes()[2]); + + VibrationEffect.Waveform scaled = initial.scale(1.1f, 200); assertEquals(200, scaled.getAmplitudes()[0]); assertEquals(0, scaled.getAmplitudes()[1]); } @@ -156,6 +173,66 @@ public class VibrationEffectTest { } } + @Test + public void testScaleComposed() { + VibrationEffect.Composed initial = (VibrationEffect.Composed) TEST_COMPOSED; + + VibrationEffect.Composed copied = initial.scale(1, 255); + assertEquals(1f, copied.getPrimitiveEffects().get(0).scale); + assertEquals(0.5f, copied.getPrimitiveEffects().get(1).scale); + assertEquals(0f, copied.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed halved = initial.scale(1, 128); + assertEquals(0.5f, halved.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertEquals(0.25f, halved.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE); + assertEquals(0f, halved.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed scaledUp = initial.scale(0.5f, 255); + assertEquals(1f, scaledUp.getPrimitiveEffects().get(0).scale); // does not scale up from 1 + assertTrue(0.5f < scaledUp.getPrimitiveEffects().get(1).scale); + assertEquals(0f, scaledUp.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed restored = scaledUp.scale(2, 255); + assertEquals(1f, restored.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertEquals(0.5f, restored.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE); + assertEquals(0f, restored.getPrimitiveEffects().get(2).scale); + + VibrationEffect.Composed scaledDown = initial.scale(2, 255); + assertEquals(1f, scaledDown.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertTrue(0.5f > scaledDown.getPrimitiveEffects().get(1).scale); + assertEquals(0f, scaledDown.getPrimitiveEffects().get(2).scale, SCALE_TOLERANCE); + + VibrationEffect.Composed changeMax = initial.scale(1f, 51); + assertEquals(0.2f, changeMax.getPrimitiveEffects().get(0).scale, SCALE_TOLERANCE); + assertEquals(0.1f, changeMax.getPrimitiveEffects().get(1).scale, SCALE_TOLERANCE); + assertEquals(0f, changeMax.getPrimitiveEffects().get(2).scale); + } + + @Test + public void testScaleComposedFailsWhenMaxAmplitudeAboveThreshold() { + try { + ((VibrationEffect.Composed) TEST_COMPOSED).scale(1.1f, 1000); + fail("Max amplitude above threshold, should throw IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testScaleAppliesSameAdjustmentsOnAllEffects() { + VibrationEffect.OneShot oneShot = new VibrationEffect.OneShot(TEST_TIMING, TEST_AMPLITUDE); + VibrationEffect.Waveform waveform = new VibrationEffect.Waveform( + new long[] { TEST_TIMING }, new int[]{ TEST_AMPLITUDE }, -1); + VibrationEffect.Composed composed = + (VibrationEffect.Composed) VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, TEST_AMPLITUDE / 255f) + .compose(); + + assertEquals(oneShot.scale(2f, 128).getAmplitude(), + waveform.scale(2f, 128).getAmplitudes()[0]); + assertEquals(oneShot.scale(2f, 128).getAmplitude() / 255f, // convert amplitude to scale + composed.scale(2f, 128).getPrimitiveEffects().get(0).scale, + SCALE_TOLERANCE); + } private Resources mockRingtoneResources() { return mockRingtoneResources(new String[] { @@ -172,9 +249,22 @@ public class VibrationEffectTest { return mockResources; } - private Context mockContext(Resources r) { - Context ctx = mock(Context.class); - when(ctx.getResources()).thenReturn(r); - return ctx; + private Context mockContext(Resources resources) { + Context context = mock(Context.class); + ContentInterface contentInterface = mock(ContentInterface.class); + ContentResolver contentResolver = ContentResolver.wrap(contentInterface); + + try { + // ContentResolver#uncanonicalize is final, so we need to mock the ContentInterface it + // delegates the call to for the tests that require matching with the mocked URIs. + when(contentInterface.uncanonicalize(any())).then( + invocation -> invocation.getArgument(0)); + when(context.getContentResolver()).thenReturn(contentResolver); + when(context.getResources()).thenReturn(resources); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + + return context; } } diff --git a/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java b/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java new file mode 100644 index 000000000000..e6ac2d6c43da --- /dev/null +++ b/core/tests/coretests/src/android/view/ScrollCaptureClientTest.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; +import static androidx.test.InstrumentationRegistry.getTargetContext; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; + +/** + * Tests of {@link ScrollCaptureClient}. + */ +@SuppressWarnings("UnnecessaryLocalVariable") +@RunWith(AndroidJUnit4.class) +public class ScrollCaptureClientTest { + + private final Point mPositionInWindow = new Point(1, 2); + private final Rect mLocalVisibleRect = new Rect(2, 3, 4, 5); + private final Rect mScrollBounds = new Rect(3, 4, 5, 6); + + private Handler mHandler; + private ScrollCaptureTarget mTarget1; + + @Mock + private Surface mSurface; + @Mock + private IScrollCaptureController mClientCallbacks; + @Mock + private View mMockView1; + @Mock + private ScrollCaptureCallback mCallback1; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mHandler = new Handler(getTargetContext().getMainLooper()); + + when(mMockView1.getHandler()).thenReturn(mHandler); + when(mMockView1.getScrollCaptureHint()).thenReturn(View.SCROLL_CAPTURE_HINT_INCLUDE); + + mTarget1 = new ScrollCaptureTarget( + mMockView1, mLocalVisibleRect, mPositionInWindow, mCallback1); + mTarget1.setScrollBounds(mScrollBounds); + } + + /** Test the DelayedAction timeout helper class works as expected. */ + @Test + public void testDelayedAction() { + Runnable action = Mockito.mock(Runnable.class); + ScrollCaptureClient.DelayedAction delayed = + new ScrollCaptureClient.DelayedAction(mHandler, 100, action); + try { + Thread.sleep(200); + } catch (InterruptedException ex) { + /* ignore */ + } + getInstrumentation().waitForIdleSync(); + assertFalse(delayed.cancel()); + assertFalse(delayed.timeoutNow()); + verify(action, times(1)).run(); + } + + /** Test the DelayedAction cancel() */ + @Test + public void testDelayedAction_cancel() { + Runnable action = Mockito.mock(Runnable.class); + ScrollCaptureClient.DelayedAction delayed = + new ScrollCaptureClient.DelayedAction(mHandler, 100, action); + try { + Thread.sleep(50); + } catch (InterruptedException ex) { + /* ignore */ + } + assertTrue(delayed.cancel()); + assertFalse(delayed.timeoutNow()); + try { + Thread.sleep(200); + } catch (InterruptedException ex) { + /* ignore */ + } + getInstrumentation().waitForIdleSync(); + verify(action, never()).run(); + } + + /** Test the DelayedAction timeoutNow() - for testing only */ + @Test + public void testDelayedAction_timeoutNow() { + Runnable action = Mockito.mock(Runnable.class); + ScrollCaptureClient.DelayedAction delayed = + new ScrollCaptureClient.DelayedAction(mHandler, 100, action); + try { + Thread.sleep(50); + } catch (InterruptedException ex) { + /* ignore */ + } + assertTrue(delayed.timeoutNow()); + assertFalse(delayed.cancel()); + getInstrumentation().waitForIdleSync(); + verify(action, times(1)).run(); + } + + /** Test creating a client with valid info */ + @Test + public void testConstruction() { + new ScrollCaptureClient(mTarget1, mClientCallbacks); + } + + /** Test creating a client fails if arguments are not valid. */ + @Test + public void testConstruction_requiresScrollBounds() { + try { + mTarget1.setScrollBounds(null); + new ScrollCaptureClient(mTarget1, mClientCallbacks); + fail("An exception was expected."); + } catch (RuntimeException ex) { + // Ignore, expected. + } + } + + @SuppressWarnings("SameParameterValue") + private static Answer<Void> runRunnable(int arg) { + return invocation -> { + Runnable r = invocation.getArgument(arg); + r.run(); + return null; + }; + } + + @SuppressWarnings("SameParameterValue") + private static Answer<Void> reportBufferSent(int sessionArg, long frameNum, Rect capturedArea) { + return invocation -> { + ScrollCaptureSession session = invocation.getArgument(sessionArg); + session.notifyBufferSent(frameNum, capturedArea); + return null; + }; + } + + /** @see ScrollCaptureClient#startCapture(Surface) */ + @Test + public void testStartCapture() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + + // Have the session start accepted immediately + doAnswer(runRunnable(1)).when(mCallback1) + .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class)); + client.startCapture(mSurface); + getInstrumentation().waitForIdleSync(); + + verify(mCallback1, times(1)) + .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class)); + verify(mClientCallbacks, times(1)).onCaptureStarted(); + verifyNoMoreInteractions(mClientCallbacks); + } + + @Test + public void testStartCaptureTimeout() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + client.startCapture(mSurface); + + // Force timeout to fire + client.getTimeoutAction().timeoutNow(); + + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class)); + } + + private void startClient(ScrollCaptureClient client) throws Exception { + doAnswer(runRunnable(1)).when(mCallback1) + .onScrollCaptureStart(any(ScrollCaptureSession.class), any(Runnable.class)); + client.startCapture(mSurface); + getInstrumentation().waitForIdleSync(); + reset(mCallback1, mClientCallbacks); + } + + /** @see ScrollCaptureClient#requestImage(Rect) */ + @Test + public void testRequestImage() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + startClient(client); + + // Stub the callback to complete the request immediately + doAnswer(reportBufferSent(/* sessionArg */ 0, /* frameNum */ 1L, new Rect(1, 2, 3, 4))) + .when(mCallback1) + .onScrollCaptureImageRequest(any(ScrollCaptureSession.class), any(Rect.class)); + + // Make the inbound binder call + client.requestImage(new Rect(1, 2, 3, 4)); + + // Wait for handler thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureImageRequest( + any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4))); + + // Wait for binder thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mClientCallbacks, times(1)).onCaptureBufferSent(eq(1L), eq(new Rect(1, 2, 3, 4))); + + verifyNoMoreInteractions(mCallback1, mClientCallbacks); + } + + @Test + public void testRequestImageTimeout() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + startClient(client); + + // Make the inbound binder call + client.requestImage(new Rect(1, 2, 3, 4)); + + // Wait for handler thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureImageRequest( + any(ScrollCaptureSession.class), eq(new Rect(1, 2, 3, 4))); + + // Force timeout to fire + client.getTimeoutAction().timeoutNow(); + getInstrumentation().waitForIdleSync(); + + // (callback not stubbed, does nothing) + // Timeout triggers request to end capture + verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class)); + verifyNoMoreInteractions(mCallback1, mClientCallbacks); + } + + /** @see ScrollCaptureClient#endCapture() */ + @Test + public void testEndCapture() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + startClient(client); + + // Stub the callback to complete the request immediately + doAnswer(runRunnable(0)) + .when(mCallback1) + .onScrollCaptureEnd(any(Runnable.class)); + + // Make the inbound binder call + client.endCapture(); + + // Wait for handler thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class)); + + // Wait for binder thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mClientCallbacks, times(1)).onConnectionClosed(); + + verifyNoMoreInteractions(mCallback1, mClientCallbacks); + } + + @Test + public void testEndCaptureTimeout() throws Exception { + final ScrollCaptureClient client = new ScrollCaptureClient(mTarget1, mClientCallbacks); + startClient(client); + + // Make the inbound binder call + client.endCapture(); + + // Wait for handler thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mCallback1, times(1)).onScrollCaptureEnd(any(Runnable.class)); + + // Force timeout to fire + client.getTimeoutAction().timeoutNow(); + + // Wait for binder thread dispatch + getInstrumentation().waitForIdleSync(); + verify(mClientCallbacks, times(1)).onConnectionClosed(); + + verifyNoMoreInteractions(mCallback1, mClientCallbacks); + } +} diff --git a/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java b/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java new file mode 100644 index 000000000000..8b21b8ecee89 --- /dev/null +++ b/core/tests/coretests/src/android/view/ScrollCaptureTargetResolverTest.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static androidx.test.InstrumentationRegistry.getTargetContext; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.LinkedList; +import java.util.function.Consumer; + +/** + * Tests of {@link ScrollCaptureTargetResolver}. + */ +@RunWith(AndroidJUnit4.class) +public class ScrollCaptureTargetResolverTest { + + private static final long TEST_TIMEOUT_MS = 2000; + private static final long RESOLVER_TIMEOUT_MS = 1000; + + private Handler mHandler; + private TargetConsumer mTargetConsumer; + + @Before + public void setUp() { + mTargetConsumer = new TargetConsumer(); + mHandler = new Handler(getTargetContext().getMainLooper()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testEmptyQueue() throws InterruptedException { + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(new LinkedList<>()); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertNull("Expected null due to empty queue", result); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testNoValidTargets() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + // Supplies scrollBounds = null + FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback(); + callback1.setScrollBounds(null); + ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50), + new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO); + + // Supplies scrollBounds = empty rect + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect()); + ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50), + new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE); + + targetQueue.add(target1); + targetQueue.add(target2); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertNull("Expected null due to no valid targets", result); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testSingleTarget() throws InterruptedException { + FakeScrollCaptureCallback callback = new FakeScrollCaptureCallback(); + ScrollCaptureTarget target = createTarget(callback, + new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_AUTO); + callback.setScrollBounds(new Rect(2, 2, 18, 18)); + + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + targetQueue.add(target); + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Excepted the same target as a result", target, result); + assertEquals("result has wrong scroll bounds", + new Rect(2, 2, 18, 18), result.getScrollBounds()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testSingleTarget_backgroundThread() throws InterruptedException { + BackgroundTestCallback callback1 = new BackgroundTestCallback(); + ScrollCaptureTarget target1 = createTarget(callback1, + new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_AUTO); + callback1.setDelay(100); + callback1.setScrollBounds(new Rect(2, 2, 18, 18)); + + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + targetQueue.add(target1); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Excepted the single target1 as a result", target1, result); + assertEquals("Result has wrong scroll bounds", + new Rect(2, 2, 18, 18), result.getScrollBounds()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testPreferNonEmptyBounds() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback(); + callback1.setScrollBounds(new Rect()); + ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50), + new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO); + + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect(0, 0, 20, 20)); + ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50), + new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE); + + FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback(); + callback3.setScrollBounds(null); + ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50), + new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO); + + targetQueue.add(target1); + targetQueue.add(target2); // scrollBounds not null or empty() + targetQueue.add(target3); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertEquals("Expected " + target2 + " as a result", target2, result); + assertEquals("result has wrong scroll bounds", + new Rect(0, 0, 20, 20), result.getScrollBounds()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testPreferHintInclude() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback(); + callback1.setScrollBounds(new Rect(0, 0, 20, 20)); + ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50), + new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO); + + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect(1, 1, 19, 19)); + ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50), + new Point(0, 20), View.SCROLL_CAPTURE_HINT_INCLUDE); + + FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback(); + callback3.setScrollBounds(new Rect(2, 2, 18, 18)); + ScrollCaptureTarget target3 = createTarget(callback3, new Rect(20, 30, 40, 50), + new Point(0, 40), View.SCROLL_CAPTURE_HINT_AUTO); + + targetQueue.add(target1); + targetQueue.add(target2); // * INCLUDE > AUTO + targetQueue.add(target3); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertEquals("input = " + targetQueue + " Expected " + target2 + + " as the result, due to hint=INCLUDE", target2, result); + assertEquals("result has wrong scroll bounds", + new Rect(1, 1, 19, 19), result.getScrollBounds()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testDescendantPreferred() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + ViewGroup targetView1 = new FakeRootView(getTargetContext(), 0, 0, 60, 60); // 60x60 + ViewGroup targetView2 = new FakeRootView(getTargetContext(), 20, 30, 40, 50); // 20x20 + ViewGroup targetView3 = new FakeRootView(getTargetContext(), 5, 5, 15, 15); // 10x10 + + targetView1.addView(targetView2); + targetView2.addView(targetView3); + + // Create first target with an unrelated parent + FakeScrollCaptureCallback callback1 = new FakeScrollCaptureCallback(); + callback1.setScrollBounds(new Rect(0, 0, 60, 60)); + ScrollCaptureTarget target1 = createTargetWithView(targetView1, callback1, + new Rect(0, 0, 60, 60), + new Point(0, 0), View.SCROLL_CAPTURE_HINT_AUTO); + + // Create second target associated with a view within parent2 + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect(0, 0, 20, 20)); + ScrollCaptureTarget target2 = createTargetWithView(targetView2, callback2, + new Rect(0, 0, 20, 20), + new Point(20, 30), View.SCROLL_CAPTURE_HINT_AUTO); + + // Create third target associated with a view within parent3 + FakeScrollCaptureCallback callback3 = new FakeScrollCaptureCallback(); + callback3.setScrollBounds(new Rect(0, 0, 15, 15)); + ScrollCaptureTarget target3 = createTargetWithView(targetView3, callback3, + new Rect(0, 0, 15, 15), + new Point(25, 35), View.SCROLL_CAPTURE_HINT_AUTO); + + targetQueue.add(target1); // auto, 60x60 + targetQueue.add(target2); // auto, 20x20 + targetQueue.add(target3); // auto, 15x15 <- innermost scrollable + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + // Test only + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Expected target3 as the result, due to relation", target3, result); + assertEquals("result has wrong scroll bounds", + new Rect(0, 0, 15, 15), result.getScrollBounds()); + } + + /** + * If a timeout expires, late results are ignored. + */ + @Test(timeout = TEST_TIMEOUT_MS) + public void testTimeout() throws InterruptedException { + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + + // callback 1, 10x10, hint=AUTO, responds immediately from bg thread + BackgroundTestCallback callback1 = new BackgroundTestCallback(); + callback1.setScrollBounds(new Rect(5, 5, 15, 15)); + ScrollCaptureTarget target1 = createTarget( + callback1, new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_AUTO); + targetQueue.add(target1); + + // callback 2, 20x20, hint=AUTO, responds after 5s from bg thread + BackgroundTestCallback callback2 = new BackgroundTestCallback(); + callback2.setScrollBounds(new Rect(0, 0, 20, 20)); + callback2.setDelay(5000); + ScrollCaptureTarget target2 = createTarget( + callback2, new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_AUTO); + targetQueue.add(target2); + + // callback 3, 20x20, hint=INCLUDE, responds after 10s from bg thread + BackgroundTestCallback callback3 = new BackgroundTestCallback(); + callback3.setScrollBounds(new Rect(0, 0, 20, 20)); + callback3.setDelay(10000); + ScrollCaptureTarget target3 = createTarget( + callback3, new Rect(20, 30, 40, 50), new Point(10, 10), + View.SCROLL_CAPTURE_HINT_INCLUDE); + targetQueue.add(target3); + + // callback 1 will be received + // callback 2 & 3 will be ignored due to timeout + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Expected target1 as the result, due to timeouts of others", target1, result); + assertEquals("result has wrong scroll bounds", + new Rect(5, 5, 15, 15), result.getScrollBounds()); + assertEquals("callback1 should have been called", + 1, callback1.getOnScrollCaptureSearchCount()); + assertEquals("callback2 should have been called", + 1, callback2.getOnScrollCaptureSearchCount()); + assertEquals("callback3 should have been called", + 1, callback3.getOnScrollCaptureSearchCount()); + } + + @Test(timeout = TEST_TIMEOUT_MS) + public void testWithCallbackMultipleReplies() throws InterruptedException { + // Calls response methods 3 times each + RepeatingCaptureCallback callback1 = new RepeatingCaptureCallback(3); + callback1.setScrollBounds(new Rect(2, 2, 18, 18)); + ScrollCaptureTarget target1 = createTarget(callback1, new Rect(20, 30, 40, 50), + new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO); + + FakeScrollCaptureCallback callback2 = new FakeScrollCaptureCallback(); + callback2.setScrollBounds(new Rect(0, 0, 20, 20)); + ScrollCaptureTarget target2 = createTarget(callback2, new Rect(20, 30, 40, 50), + new Point(10, 10), View.SCROLL_CAPTURE_HINT_AUTO); + + LinkedList<ScrollCaptureTarget> targetQueue = new LinkedList<>(); + targetQueue.add(target1); + targetQueue.add(target2); + + ScrollCaptureTargetResolver resolver = new ScrollCaptureTargetResolver(targetQueue); + resolver.start(mHandler, RESOLVER_TIMEOUT_MS, mTargetConsumer); + + resolver.waitForResult(); + + ScrollCaptureTarget result = mTargetConsumer.getLastValue(); + assertSame("Expected target2 as the result, due to hint=INCLUDE", target2, result); + assertEquals("result has wrong scroll bounds", + new Rect(0, 0, 20, 20), result.getScrollBounds()); + assertEquals("callback1 should have been called once", + 1, callback1.getOnScrollCaptureSearchCount()); + assertEquals("callback2 should have been called once", + 1, callback2.getOnScrollCaptureSearchCount()); + } + + private static class TargetConsumer implements Consumer<ScrollCaptureTarget> { + volatile ScrollCaptureTarget mResult; + int mAcceptCount; + + ScrollCaptureTarget getLastValue() { + return mResult; + } + + int acceptCount() { + return mAcceptCount; + } + + @Override + public void accept(@Nullable ScrollCaptureTarget t) { + mAcceptCount++; + mResult = t; + } + } + + private void setupTargetView(View view, Rect localVisibleRect, int scrollCaptureHint) { + view.setScrollCaptureHint(scrollCaptureHint); + view.onVisibilityAggregated(true); + // Treat any offset as padding, outset localVisibleRect on all sides and use this as + // child bounds + Rect bounds = new Rect(localVisibleRect); + bounds.inset(-bounds.left, -bounds.top, bounds.left, bounds.top); + view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom); + view.onVisibilityAggregated(true); + } + + private ScrollCaptureTarget createTarget(ScrollCaptureCallback callback, Rect localVisibleRect, + Point positionInWindow, int scrollCaptureHint) { + View mockView = new View(getTargetContext()); + return createTargetWithView(mockView, callback, localVisibleRect, positionInWindow, + scrollCaptureHint); + } + + private ScrollCaptureTarget createTargetWithView(View view, ScrollCaptureCallback callback, + Rect localVisibleRect, Point positionInWindow, int scrollCaptureHint) { + setupTargetView(view, localVisibleRect, scrollCaptureHint); + return new ScrollCaptureTarget(view, localVisibleRect, positionInWindow, callback); + } + + + static class FakeRootView extends ViewGroup implements ViewParent { + FakeRootView(Context context, int l, int t, int r, int b) { + super(context); + layout(l, t, r, b); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } + } + + static class FakeScrollCaptureCallback implements ScrollCaptureCallback { + private Rect mScrollBounds; + private long mDelayMillis; + private int mOnScrollCaptureSearchCount; + + public int getOnScrollCaptureSearchCount() { + return mOnScrollCaptureSearchCount; + } + + @Override + public void onScrollCaptureSearch(Consumer<Rect> onReady) { + mOnScrollCaptureSearchCount++; + run(() -> { + Rect b = getScrollBounds(); + onReady.accept(b); + }); + } + + @Override + public void onScrollCaptureStart(ScrollCaptureSession session, Runnable onReady) { + run(onReady); + } + + @Override + public void onScrollCaptureImageRequest(ScrollCaptureSession session, Rect captureArea) { + run(() -> session.notifyBufferSent(0, captureArea)); + } + + @Override + public void onScrollCaptureEnd(Runnable onReady) { + run(onReady); + } + + public void setScrollBounds(@Nullable Rect scrollBounds) { + mScrollBounds = scrollBounds; + } + + public void setDelay(long delayMillis) { + mDelayMillis = delayMillis; + } + + protected Rect getScrollBounds() { + return mScrollBounds; + } + + protected void run(Runnable r) { + delay(); + r.run(); + } + + protected void delay() { + if (mDelayMillis > 0) { + try { + Thread.sleep(mDelayMillis); + } catch (InterruptedException e) { + // Ignore + } + } + } + } + + static class RepeatingCaptureCallback extends FakeScrollCaptureCallback { + private int mRepeatCount; + + RepeatingCaptureCallback(int repeatCount) { + mRepeatCount = repeatCount; + } + + protected void run(Runnable r) { + delay(); + for (int i = 0; i < mRepeatCount; i++) { + r.run(); + } + } + } + + /** Response to async calls on an arbitrary background thread */ + static class BackgroundTestCallback extends FakeScrollCaptureCallback { + static int sCount = 0; + private void runOnBackgroundThread(Runnable r) { + final Runnable target = () -> { + delay(); + r.run(); + }; + Thread t = new Thread(target); + synchronized (BackgroundTestCallback.this) { + sCount++; + } + t.setName("Background-Thread-" + sCount); + t.start(); + } + + @Override + protected void run(Runnable r) { + runOnBackgroundThread(r); + } + } +} diff --git a/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java b/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java new file mode 100644 index 000000000000..3af0533e763c --- /dev/null +++ b/core/tests/coretests/src/android/view/ViewGroupScrollCaptureTest.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.testng.AssertJUnit.assertSame; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.FlakyTest; +import androidx.test.filters.MediumTest; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * Exercises Scroll Capture search in {@link ViewGroup}. + */ +@Presubmit +@SmallTest +@FlakyTest(detail = "promote once confirmed flake-free") +@RunWith(MockitoJUnitRunner.class) +public class ViewGroupScrollCaptureTest { + + @Mock + ScrollCaptureCallback mMockCallback; + @Mock + ScrollCaptureCallback mMockCallback2; + + /** Make sure the hint flags are saved and loaded correctly. */ + @Test + public void testSetScrollCaptureHint() throws Exception { + final Context context = getInstrumentation().getContext(); + final MockViewGroup viewGroup = new MockViewGroup(context); + + assertNotNull(viewGroup); + assertEquals("Default scroll capture hint flags should be [SCROLL_CAPTURE_HINT_AUTO]", + ViewGroup.SCROLL_CAPTURE_HINT_AUTO, viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE, viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE, viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS, + viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE + | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_INCLUDE + | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS, + viewGroup.getScrollCaptureHint()); + + viewGroup.setScrollCaptureHint(ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE + | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS); + assertEquals("The scroll capture hint was not stored correctly.", + ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE + | ViewGroup.SCROLL_CAPTURE_HINT_EXCLUDE_DESCENDANTS, + viewGroup.getScrollCaptureHint()); + } + + /** + * Ensure a ViewGroup with 'scrollCaptureHint=auto', but no ScrollCaptureCallback set dispatches + * correctly. Verifies that the framework helper is called. Verifies a that non-null callback + * return results in an expected target in the results. + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_noCallback_hintAuto() throws Exception { + final Context context = getInstrumentation().getContext(); + final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200); + + // When system internal scroll capture is requested, this callback is returned. + viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback); + + Rect localVisibleRect = new Rect(0, 0, 200, 200); + Point windowOffset = new Point(); + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // Verify the system checked for fallback support + viewGroup.assertDispatchScrollCaptureCount(1); + viewGroup.assertLastDispatchScrollCaptureArgs(localVisibleRect, windowOffset); + + // Verify the target is as expected. + assertEquals(1, targetList.size()); + ScrollCaptureTarget target = targetList.get(0); + assertSame("Target has the wrong callback", mMockCallback, target.getCallback()); + assertSame("Target has the wrong View", viewGroup, target.getContainingView()); + assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO, + target.getContainingView().getScrollCaptureHint()); + } + + /** + * Ensure a ViewGroup with 'scrollCaptureHint=exclude' is ignored. The Framework helper is + * stubbed to return a callback. Verifies that the framework helper is not called (because of + * exclude), and no scroll capture target is added to the results. + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_noCallback_hintExclude() throws Exception { + final Context context = getInstrumentation().getContext(); + final MockViewGroup viewGroup = + new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE); + + // When system internal scroll capture is requested, this callback is returned. + viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback); + + Rect localVisibleRect = new Rect(0, 0, 200, 200); + Point windowOffset = new Point(); + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // Verify the results. + assertEquals("Target list size should be zero.", 0, targetList.size()); + } + + /** + * Ensure that a ViewGroup with 'scrollCaptureHint=auto', and a scroll capture callback set + * dispatches as expected. Also verifies that the system fallback support is not called, and the + * the returned target is constructed correctly. + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_withCallback_hintAuto() throws Exception { + final Context context = getInstrumentation().getContext(); + MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200); + + // With an already provided scroll capture callback + viewGroup.setScrollCaptureCallback(mMockCallback); + + // When system internal scroll capture is requested, this callback is returned. + viewGroup.setScrollCaptureCallbackInternalForTest(mMockCallback); + + Rect localVisibleRect = new Rect(0, 0, 200, 200); + Point windowOffset = new Point(); + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch to the ViewGroup + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // Confirm that framework support was not requested, + // because this view already had a callback set. + viewGroup.assertCreateScrollCaptureCallbackInternalCount(0); + + // Verify the target is as expected. + assertEquals(1, targetList.size()); + ScrollCaptureTarget target = targetList.get(0); + assertSame("Target has the wrong callback", mMockCallback, target.getCallback()); + assertSame("Target has the wrong View", viewGroup, target.getContainingView()); + assertEquals("Target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO, + target.getContainingView().getScrollCaptureHint()); + } + + /** + * Ensure a ViewGroup with a callback set, but 'scrollCaptureHint=exclude' is ignored. The + * exclude flag takes precedence. Verifies that the framework helper is not called (because of + * exclude, and a callback being set), and no scroll capture target is added to the results. + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_withCallback_hintExclude() throws Exception { + final Context context = getInstrumentation().getContext(); + MockViewGroup viewGroup = + new MockViewGroup(context, 0, 0, 200, 200, View.SCROLL_CAPTURE_HINT_EXCLUDE); + // With an already provided scroll capture callback + viewGroup.setScrollCaptureCallback(mMockCallback); + + Rect localVisibleRect = new Rect(0, 0, 200, 200); + Point windowOffset = new Point(); + LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch to the ViewGroup itself + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // Confirm that framework support was not requested, because this view is excluded. + // (And because this view has a callback set.) + viewGroup.assertCreateScrollCaptureCallbackInternalCount(0); + + // Has callback, but hint=excluded, so excluded. + assertTrue(targetList.isEmpty()); + } + + /** + * Test scroll capture search dispatch to child views. + * <p> + * Verifies computation of child visible bounds. + * TODO: with scrollX / scrollY, split up into discrete tests + */ + @MediumTest + @Test + public void testDispatchScrollCaptureSearch_toChildren() throws Exception { + final Context context = getInstrumentation().getContext(); + final MockViewGroup viewGroup = new MockViewGroup(context, 0, 0, 200, 200); + + Rect localVisibleRect = new Rect(25, 50, 175, 150); + Point windowOffset = new Point(0, 0); + + // visible area + // |<- l=25, | + // | r=175 ->| + // +--------------------------+ + // | view1 (0, 0, 200, 25) | + // +---------------+----------+ + // | | | + // | view2 | view4 | --+ + // | (0, 25, | (inv) | | visible area + // | 150, 100)| | | + // +---------------+----------+ | t=50, b=150 + // | view3 | view5 | | + // | (0, 100 |(150, 100 | --+ + // | 200, 200) | 200, 200)| + // | | | + // | | | + // +---------------+----------+ (200,200) + + // View 1 is clipped and not visible. + final MockView view1 = new MockView(context, 0, 0, 200, 25); + viewGroup.addView(view1); + + // View 2 is partially visible. + final MockView view2 = new MockView(context, 0, 25, 150, 100); + viewGroup.addView(view2); + + // View 3 is partially visible. + // Pretend View3 can scroll by having framework provide fallback support + final MockView view3 = new MockView(context, 0, 100, 200, 200); + // When system internal scroll capture is requested for this view, return this callback. + view3.setScrollCaptureCallbackInternalForTest(mMockCallback); + viewGroup.addView(view3); + + // View 4 is invisible and should be ignored. + final MockView view4 = new MockView(context, 150, 25, 200, 100, View.INVISIBLE); + viewGroup.addView(view4); + + // View 4 is invisible and should be ignored. + final MockView view5 = new MockView(context, 150, 100, 200, 200); + // When system internal scroll capture is requested for this view, return this callback. + view5.setScrollCaptureCallback(mMockCallback2); + view5.setScrollCaptureHint(View.SCROLL_CAPTURE_HINT_INCLUDE); + viewGroup.addView(view5); + + // Where targets are added + final LinkedList<ScrollCaptureTarget> targetList = new LinkedList<>(); + + // Dispatch to the ViewGroup + viewGroup.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targetList); + + // View 1 is entirely clipped by the parent and not visible, dispatch + // skips this view entirely. + view1.assertDispatchScrollCaptureSearchCount(0); + view1.assertCreateScrollCaptureCallbackInternalCount(0); + + // View 2, verify the computed localVisibleRect and windowOffset are correctly transformed + // to the child coordinate space + view2.assertDispatchScrollCaptureSearchCount(1); + view2.assertDispatchScrollCaptureSearchLastArgs( + new Rect(25, 25, 150, 75), new Point(0, 25)); + // No callback set, so the framework is asked for support + view2.assertCreateScrollCaptureCallbackInternalCount(1); + + // View 3, verify the computed localVisibleRect and windowOffset are correctly transformed + // to the child coordinate space + view3.assertDispatchScrollCaptureSearchCount(1); + view3.assertDispatchScrollCaptureSearchLastArgs( + new Rect(25, 0, 175, 50), new Point(0, 100)); + // No callback set, so the framework is asked for support + view3.assertCreateScrollCaptureCallbackInternalCount(1); + + // view4 is invisible, so it should be skipped entirely. + view4.assertDispatchScrollCaptureSearchCount(0); + view4.assertCreateScrollCaptureCallbackInternalCount(0); + + // view5 is partially visible + view5.assertDispatchScrollCaptureSearchCount(1); + view5.assertDispatchScrollCaptureSearchLastArgs( + new Rect(0, 0, 25, 50), new Point(150, 100)); + // view5 has a callback set on it, so internal framework support should not be consulted. + view5.assertCreateScrollCaptureCallbackInternalCount(0); + + // 2 views should have been returned, view3 & view5 + assertEquals(2, targetList.size()); + + ScrollCaptureTarget target = targetList.get(0); + assertSame("First target has the wrong View", view3, target.getContainingView()); + assertSame("First target has the wrong callback", mMockCallback, target.getCallback()); + assertEquals("First target hint is incorrect", View.SCROLL_CAPTURE_HINT_AUTO, + target.getContainingView().getScrollCaptureHint()); + + target = targetList.get(1); + assertSame("Second target has the wrong View", view5, target.getContainingView()); + assertSame("Second target has the wrong callback", mMockCallback2, target.getCallback()); + assertEquals("Second target hint is incorrect", View.SCROLL_CAPTURE_HINT_INCLUDE, + target.getContainingView().getScrollCaptureHint()); + } + + public static final class MockView extends View { + private ScrollCaptureCallback mInternalCallback; + + private int mDispatchScrollCaptureSearchNumCalls; + private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect; + private Point mDispatchScrollCaptureSearchLastWindowOffset; + private int mCreateScrollCaptureCallbackInternalCount; + + MockView(Context context) { + this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0); + } + + MockView(Context context, int left, int top, int right, int bottom) { + this(context, left, top, right, bottom, View.VISIBLE); + } + + MockView(Context context, int left, int top, int right, int bottom, int visibility) { + super(context); + setVisibility(visibility); + setFrame(left, top, right, bottom); + } + + public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) { + mInternalCallback = internal; + } + + void assertDispatchScrollCaptureSearchCount(int count) { + assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch", + count, mDispatchScrollCaptureSearchNumCalls); + } + + void assertDispatchScrollCaptureSearchLastArgs(Rect localVisibleRect, Point windowOffset) { + assertEquals("arg localVisibleRect was incorrect.", + localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect); + assertEquals("arg windowOffset was incorrect.", + windowOffset, mDispatchScrollCaptureSearchLastWindowOffset); + } + + void assertCreateScrollCaptureCallbackInternalCount(int count) { + assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal", + count, mCreateScrollCaptureCallbackInternalCount); + } + + void reset() { + mDispatchScrollCaptureSearchNumCalls = 0; + mDispatchScrollCaptureSearchLastWindowOffset = null; + mDispatchScrollCaptureSearchLastLocalVisibleRect = null; + mCreateScrollCaptureCallbackInternalCount = 0; + + } + + @Override + public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset, + Queue<ScrollCaptureTarget> targets) { + mDispatchScrollCaptureSearchNumCalls++; + mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect); + mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset); + super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets); + } + + @Override + @Nullable + public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect, + Point offsetInWindow) { + mCreateScrollCaptureCallbackInternalCount++; + return mInternalCallback; + } + } + + public static final class MockViewGroup extends ViewGroup { + private ScrollCaptureCallback mInternalCallback; + private int mDispatchScrollCaptureSearchNumCalls; + private Rect mDispatchScrollCaptureSearchLastLocalVisibleRect; + private Point mDispatchScrollCaptureSearchLastWindowOffset; + private int mCreateScrollCaptureCallbackInternalCount; + + + MockViewGroup(Context context) { + this(context, /* left */ 0, /* top */0, /* right */ 0, /* bottom */0); + } + + MockViewGroup(Context context, int left, int top, int right, int bottom) { + this(context, left, top, right, bottom, View.SCROLL_CAPTURE_HINT_AUTO); + } + + MockViewGroup(Context context, int left, int top, int right, int bottom, + int scrollCaptureHint) { + super(context); + setScrollCaptureHint(scrollCaptureHint); + setFrame(left, top, right, bottom); + } + + public void setScrollCaptureCallbackInternalForTest(ScrollCaptureCallback internal) { + mInternalCallback = internal; + } + + void assertDispatchScrollCaptureSearchCount(int count) { + assertEquals("Unexpected number of calls to dispatchScrollCaptureSearch", + count, mDispatchScrollCaptureSearchNumCalls); + } + + @Override + @Nullable + public ScrollCaptureCallback createScrollCaptureCallbackInternal(Rect localVisibleRect, + Point offsetInWindow) { + mCreateScrollCaptureCallbackInternalCount++; + return mInternalCallback; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + // We don't layout this view. + } + + void assertDispatchScrollCaptureCount(int count) { + assertEquals(count, mDispatchScrollCaptureSearchNumCalls); + } + + void assertLastDispatchScrollCaptureArgs(Rect localVisibleRect, Point windowOffset) { + assertEquals("arg localVisibleRect to dispatchScrollCaptureCallback was incorrect.", + localVisibleRect, mDispatchScrollCaptureSearchLastLocalVisibleRect); + assertEquals("arg windowOffset to dispatchScrollCaptureCallback was incorrect.", + windowOffset, mDispatchScrollCaptureSearchLastWindowOffset); + } + void assertCreateScrollCaptureCallbackInternalCount(int count) { + assertEquals("Unexpected number of calls to createScrollCaptureCallackInternal", + count, mCreateScrollCaptureCallbackInternalCount); + } + + void reset() { + mDispatchScrollCaptureSearchNumCalls = 0; + mDispatchScrollCaptureSearchLastWindowOffset = null; + mDispatchScrollCaptureSearchLastLocalVisibleRect = null; + mCreateScrollCaptureCallbackInternalCount = 0; + } + + @Override + public void dispatchScrollCaptureSearch(Rect localVisibleRect, Point windowOffset, + Queue<ScrollCaptureTarget> targets) { + mDispatchScrollCaptureSearchNumCalls++; + mDispatchScrollCaptureSearchLastLocalVisibleRect = new Rect(localVisibleRect); + mDispatchScrollCaptureSearchLastWindowOffset = new Point(windowOffset); + super.dispatchScrollCaptureSearch(localVisibleRect, windowOffset, targets); + } + } +} diff --git a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java index 769c57835662..43590bae6770 100644 --- a/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/IntentForwarderActivityTest.java @@ -68,6 +68,8 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; @RunWith(AndroidJUnit4.class) public class IntentForwarderActivityTest { @@ -633,6 +635,11 @@ public class IntentForwarderActivityTest { public void onCreate(@Nullable Bundle savedInstanceState) { getIntent().setComponent(sComponentName); super.onCreate(savedInstanceState); + try { + mExecutorService.awaitTermination(/* timeout= */ 30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + e.printStackTrace(); + } } @Override @@ -648,12 +655,6 @@ public class IntentForwarderActivityTest { } @Override - public void startActivity(Intent intent) { - mStartActivityIntent = intent; - mUserIdActivityLaunchedIn = getUserId(); - } - - @Override protected MetricsLogger getMetricsLogger() { return mMetricsLogger; } @@ -677,7 +678,8 @@ public class IntentForwarderActivityTest { } @Override - public ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId) { + public CompletableFuture<ResolveInfo> resolveActivityAsUser( + Intent intent, int flags, int userId) { ActivityInfo activityInfo = new ActivityInfo(); activityInfo.packageName = sPackageName; activityInfo.name = sActivityName; @@ -686,7 +688,7 @@ public class IntentForwarderActivityTest { ResolveInfo resolveInfo = new ResolveInfo(); resolveInfo.activityInfo = activityInfo; - return resolveInfo; + return CompletableFuture.completedFuture(resolveInfo); } @Override diff --git a/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java new file mode 100644 index 000000000000..63a68e99b788 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/view/ScrollViewCaptureHelperTest.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2020 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.view; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import static androidx.test.InstrumentationRegistry.getContext; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.test.annotation.UiThreadTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Random; + +public class ScrollViewCaptureHelperTest { + + private FrameLayout mParent; + private ScrollView mTarget; + private LinearLayout mContent; + private WindowManager mWm; + + private WindowManager.LayoutParams mWindowLayoutParams; + + private static final int CHILD_VIEWS = 12; + public static final int CHILD_VIEW_HEIGHT = 300; + + private static final int WINDOW_WIDTH = 800; + private static final int WINDOW_HEIGHT = 1200; + + private static final int CAPTURE_HEIGHT = 600; + + private Random mRandom; + + private static float sDensity; + + @BeforeClass + public static void setUpClass() { + sDensity = getContext().getResources().getDisplayMetrics().density; + } + + @Before + @UiThreadTest + public void setUp() { + mRandom = new Random(); + mParent = new FrameLayout(getContext()); + + mTarget = new ScrollView(getContext()); + mParent.addView(mTarget, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + mContent = new LinearLayout(getContext()); + mContent.setOrientation(LinearLayout.VERTICAL); + mTarget.addView(mContent, new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); + + for (int i = 0; i < CHILD_VIEWS; i++) { + TextView view = new TextView(getContext()); + view.setText("Child #" + i); + view.setTextColor(Color.WHITE); + view.setTextSize(30f); + view.setBackgroundColor(Color.rgb(mRandom.nextFloat(), mRandom.nextFloat(), + mRandom.nextFloat())); + mContent.addView(view, new ViewGroup.LayoutParams(MATCH_PARENT, CHILD_VIEW_HEIGHT)); + } + + // Window -> Parent -> Target -> Content + + mWm = getContext().getSystemService(WindowManager.class); + + // Setup the window that we are going to use + mWindowLayoutParams = new WindowManager.LayoutParams(WINDOW_WIDTH, WINDOW_HEIGHT, + TYPE_APPLICATION_OVERLAY, FLAG_NOT_TOUCHABLE, PixelFormat.OPAQUE); + mWindowLayoutParams.setTitle("ScrollViewCaptureHelper"); + mWindowLayoutParams.gravity = Gravity.CENTER; + mWm.addView(mParent, mWindowLayoutParams); + } + + @After + @UiThreadTest + public void tearDown() { + mWm.removeViewImmediate(mParent); + } + + @Test + @UiThreadTest + public void onPrepareForStart() { + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + } + + static void assertEmpty(Rect r) { + if (r != null && !r.isEmpty()) { + fail("Not true that " + r + " is empty"); + } + } + + static void assertContains(Rect parent, Rect child) { + if (!parent.contains(child)) { + fail("Not true that " + parent + " contains " + child); + } + } + + static void assertRectEquals(Rect parent, Rect child) { + if (!parent.equals(child)) { + fail("Not true that " + parent + " is equal to " + child); + } + } + + static Rect getVisibleRect(View v) { + Rect r = new Rect(0, 0, v.getWidth(), v.getHeight()); + v.getLocalVisibleRect(r); + return r; + } + + + static int assertScrollToY(View v, int scrollY) { + v.scrollTo(0, scrollY); + int dest = v.getScrollY(); + assertEquals(scrollY, dest); + return scrollY; + } + + + static void assertCapturedAreaCompletelyVisible(int startScrollY, Rect requestRect, + Rect localVisibleNow) { + Rect captured = new Rect(localVisibleNow); + captured.offset(0, -startScrollY); // make relative + + if (!captured.contains(requestRect)) { + fail("Not true that all of " + requestRect + " is contained by " + captured); + } + } + static void assertCapturedAreaPartiallyVisible(int startScrollY, Rect requestRect, + Rect localVisibleNow) { + Rect captured = new Rect(localVisibleNow); + captured.offset(0, -startScrollY); // make relative + + if (!Rect.intersects(captured, requestRect)) { + fail("Not true that any of " + requestRect + " intersects " + captured); + } + } + + @Test + @UiThreadTest + public void onScrollRequested_up_fromTop() { + final int startScrollY = assertScrollToY(mTarget, 0); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + assertTrue(scrollBounds.height() > CAPTURE_HEIGHT); + + Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + + // The result is an empty rectangle and no scrolling, since it + // is not possible to physically scroll further up to make the + // requested area visible at all (it doesn't exist). + assertEmpty(result); + } + + @Test + @UiThreadTest + public void onScrollRequested_down_fromTop() { + final int startScrollY = assertScrollToY(mTarget, 0); + + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + assertTrue(scrollBounds.height() > CAPTURE_HEIGHT); + + // Capture between y = +1200 to +1500 pixels BELOW current top + Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(), + WINDOW_HEIGHT + CAPTURE_HEIGHT); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + assertRectEquals(request, result); + + assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + + @Test + @UiThreadTest + public void onScrollRequested_up_fromMiddle() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0); + + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + + assertRectEquals(request, result); + + assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + @Test + @UiThreadTest + public void onScrollRequested_down_fromMiddle() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(), + WINDOW_HEIGHT + CAPTURE_HEIGHT); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + assertRectEquals(request, result); + + assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + @Test + @UiThreadTest + public void onScrollRequested_up_fromBottom() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + Rect request = new Rect(0, -CAPTURE_HEIGHT, scrollBounds.width(), 0); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + assertRectEquals(request, result); + + assertCapturedAreaCompletelyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + @Test + @UiThreadTest + public void onScrollRequested_down_fromBottom() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + Rect request = new Rect(0, WINDOW_HEIGHT, scrollBounds.width(), + WINDOW_HEIGHT + CAPTURE_HEIGHT); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + + // The result is an empty rectangle and no scrolling, since it + // is not possible to physically scroll further down to make the + // requested area visible at all (it doesn't exist). + assertEmpty(result); + } + + @Test + @UiThreadTest + public void onScrollRequested_offTopEdge() { + final int startScrollY = assertScrollToY(mTarget, 0); + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + // Create a request which lands halfway off the top of the content + //from -1500 to -900, (starting at 1200 = -300 to +300 within the content) + int top = 0; + Rect request = new Rect( + 0, top - (CAPTURE_HEIGHT / 2), + scrollBounds.width(), top + (CAPTURE_HEIGHT / 2)); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + // The result is a partial result + Rect expectedResult = new Rect(request); + expectedResult.top += 300; // top half clipped + assertRectEquals(expectedResult, result); + assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent)); + } + + @Test + @UiThreadTest + public void onScrollRequested_offBottomEdge() { + final int startScrollY = assertScrollToY(mTarget, WINDOW_HEIGHT * 2); // 2400 + + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + Rect scrollBounds = svc.onComputeScrollBounds(mTarget); + svc.onPrepareForStart(mTarget, scrollBounds); + + // Create a request which lands halfway off the bottom of the content + //from 600 to to 1200, (starting at 2400 = 3000 to 3600 within the content) + + int bottom = WINDOW_HEIGHT; + Rect request = new Rect( + 0, bottom - (CAPTURE_HEIGHT / 2), + scrollBounds.width(), bottom + (CAPTURE_HEIGHT / 2)); + + Rect result = svc.onScrollRequested(mTarget, scrollBounds, request); + + Rect expectedResult = new Rect(request); + expectedResult.bottom -= 300; // bottom half clipped + assertRectEquals(expectedResult, result); + assertCapturedAreaPartiallyVisible(startScrollY, request, getVisibleRect(mContent)); + + } + + @Test + @UiThreadTest + public void onPrepareForEnd() { + ScrollViewCaptureHelper svc = new ScrollViewCaptureHelper(); + svc.onPrepareForEnd(mTarget); + } +} diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java index cd6b3af5fa6d..fe33cd80f735 100644 --- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java +++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java @@ -36,6 +36,7 @@ import android.graphics.Insets; import android.graphics.Rect; import android.os.Handler; import android.os.Looper; +import android.view.WindowManager; import androidx.test.runner.AndroidJUnit4; @@ -91,7 +92,8 @@ public final class ScreenshotHelperTest { public void testProvidedImageScreenshot() { mScreenshotHelper.provideScreenshot( Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888), new Rect(), - Insets.of(0, 0, 0, 0), 1, mHandler, null); + Insets.of(0, 0, 0, 0), 1, + WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null); } @Test diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 18086ec0313e..c5ac451a9539 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -283,6 +283,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-1517908912": { + "message": "requestScrollCapture: caught exception dispatching to window.token=%s", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "-1515151503": { "message": ">>> OPEN TRANSACTION removeReplacedWindows", "level": "INFO", @@ -1441,6 +1447,12 @@ "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimation.java" }, + "646981048": { + "message": "Invalid displayId for requestScrollCapture: %d", + "level": "ERROR", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "662572728": { "message": "Attempted to add a toast window with bad token %s. Aborting.", "level": "WARN", @@ -1597,6 +1609,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1046922686": { + "message": "requestScrollCapture: caught exception dispatching callback: %s", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "1051545910": { "message": "Exit animation finished in %s: remove=%b", "level": "VERBOSE", diff --git a/data/keyboards/Vendor_18d1_Product_0200.kcm b/data/keyboards/Vendor_18d1_Product_0200.kcm new file mode 100644 index 000000000000..231fac6b48b7 --- /dev/null +++ b/data/keyboards/Vendor_18d1_Product_0200.kcm @@ -0,0 +1,48 @@ +# Copyright (C) 2020 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. + +type FULL + +key BUTTON_A { + base: fallback DPAD_CENTER +} + +key BUTTON_B { + base: fallback BACK +} + +key BUTTON_X { + base: fallback DPAD_CENTER +} + +key BUTTON_Y { + base: fallback BACK +} + +key BUTTON_THUMBL { + base: fallback DPAD_CENTER +} + +key BUTTON_THUMBR { + base: fallback DPAD_CENTER +} + +key BUTTON_SELECT { + base: fallback MENU +} + +key BUTTON_MODE { + base: fallback MENU +} + diff --git a/data/keyboards/Vendor_18d1_Product_0200.kl b/data/keyboards/Vendor_18d1_Product_0200.kl new file mode 100644 index 000000000000..d30bcc60e663 --- /dev/null +++ b/data/keyboards/Vendor_18d1_Product_0200.kl @@ -0,0 +1,71 @@ +# Copyright (C) 2020 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. + +# +# Keyboard map for the android virtual remote running as a gamepad +# + +key 0x130 BUTTON_A +key 0x131 BUTTON_B +key 0x133 BUTTON_X +key 0x134 BUTTON_Y + +key 0x136 BUTTON_L2 +key 0x137 BUTTON_R2 +key 0x138 BUTTON_L1 +key 0x139 BUTTON_R1 + +key 0x13a BUTTON_SELECT +key 0x13b BUTTON_START +key 0x13c BUTTON_MODE + +key 0x13d BUTTON_THUMBL +key 0x13e BUTTON_THUMBR + +key 103 DPAD_UP +key 108 DPAD_DOWN +key 105 DPAD_LEFT +key 106 DPAD_RIGHT + +# Generic usage buttons +key 0x2c0 BUTTON_1 +key 0x2c1 BUTTON_2 +key 0x2c2 BUTTON_3 +key 0x2c3 BUTTON_4 +key 0x2c4 BUTTON_5 +key 0x2c5 BUTTON_6 +key 0x2c6 BUTTON_7 +key 0x2c7 BUTTON_8 +key 0x2c8 BUTTON_9 +key 0x2c9 BUTTON_10 +key 0x2ca BUTTON_11 +key 0x2cb BUTTON_12 +key 0x2cc BUTTON_13 +key 0x2cd BUTTON_14 +key 0x2ce BUTTON_15 +key 0x2cf BUTTON_16 + +# assistant buttons +key 0x246 VOICE_ASSIST +key 0x247 ASSIST + +axis 0x00 X +axis 0x01 Y +axis 0x02 Z +axis 0x05 RZ +axis 0x09 RTRIGGER +axis 0x0a LTRIGGER +axis 0x10 HAT_X +axis 0x11 HAT_Y + diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java index cd878c5a77cb..228d03a1dd10 100644 --- a/graphics/java/android/graphics/SurfaceTexture.java +++ b/graphics/java/android/graphics/SurfaceTexture.java @@ -120,7 +120,7 @@ public class SurfaceTexture { /** * Construct a new SurfaceTexture to stream images to a given OpenGL texture. - * + * <p> * In single buffered mode the application is responsible for serializing access to the image * content buffer. Each time the image content is to be updated, the * {@link #releaseTexImage()} method must be called before the image content producer takes @@ -143,7 +143,7 @@ public class SurfaceTexture { /** * Construct a new SurfaceTexture to stream images to a given OpenGL texture. - * + * <p> * In single buffered mode the application is responsible for serializing access to the image * content buffer. Each time the image content is to be updated, the * {@link #releaseTexImage()} method must be called before the image content producer takes @@ -152,7 +152,7 @@ public class SurfaceTexture { * must be called before each ANativeWindow_lock, or that call will fail. When producing * image content with OpenGL ES, {@link #releaseTexImage()} must be called before the first * OpenGL ES function call each frame. - * + * <p> * Unlike {@link #SurfaceTexture(int, boolean)}, which takes an OpenGL texture object name, * this constructor creates the SurfaceTexture in detached mode. A texture name must be passed * in using {@link #attachToGLContext} before calling {@link #releaseTexImage()} and producing @@ -222,15 +222,15 @@ public class SurfaceTexture { * method. Both video and camera based image producers do override the size. This method may * be used to set the image size when producing images with {@link android.graphics.Canvas} (via * {@link android.view.Surface#lockCanvas}), or OpenGL ES (via an EGLSurface). - * + * <p> * The new default buffer size will take effect the next time the image producer requests a * buffer to fill. For {@link android.graphics.Canvas} this will be the next time {@link * android.view.Surface#lockCanvas} is called. For OpenGL ES, the EGLSurface should be * destroyed (via eglDestroySurface), made not-current (via eglMakeCurrent), and then recreated - * (via eglCreateWindowSurface) to ensure that the new default size has taken effect. - * + * (via {@code eglCreateWindowSurface}) to ensure that the new default size has taken effect. + * <p> * The width and height parameters must be no greater than the minimum of - * GL_MAX_VIEWPORT_DIMS and GL_MAX_TEXTURE_SIZE (see + * {@code GL_MAX_VIEWPORT_DIMS} and {@code GL_MAX_TEXTURE_SIZE} (see * {@link javax.microedition.khronos.opengles.GL10#glGetIntegerv glGetIntegerv}). * An error due to invalid dimensions might not be reported until * updateTexImage() is called. @@ -242,7 +242,7 @@ public class SurfaceTexture { /** * Update the texture image to the most recent frame from the image stream. This may only be * called while the OpenGL ES context that owns the texture is current on the calling thread. - * It will implicitly bind its texture to the GL_TEXTURE_EXTERNAL_OES texture target. + * It will implicitly bind its texture to the {@code GL_TEXTURE_EXTERNAL_OES} texture target. */ public void updateTexImage() { nativeUpdateTexImage(); @@ -251,6 +251,7 @@ public class SurfaceTexture { /** * Releases the the texture content. This is needed in single buffered mode to allow the image * content producer to take ownership of the image buffer. + * <p> * For more information see {@link #SurfaceTexture(int, boolean)}. */ public void releaseTexImage() { @@ -263,7 +264,7 @@ public class SurfaceTexture { * ES texture object will be deleted as a result of this call. After calling this method all * calls to {@link #updateTexImage} will throw an {@link java.lang.IllegalStateException} until * a successful call to {@link #attachToGLContext} is made. - * + * <p> * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES * contexts. Note, however, that the image contents are only accessible from one OpenGL ES * context at a time. @@ -279,8 +280,8 @@ public class SurfaceTexture { * Attach the SurfaceTexture to the OpenGL ES context that is current on the calling thread. A * new OpenGL ES texture object is created and populated with the SurfaceTexture image frame * that was current at the time of the last call to {@link #detachFromGLContext}. This new - * texture is bound to the GL_TEXTURE_EXTERNAL_OES texture target. - * + * texture is bound to the {@code GL_TEXTURE_EXTERNAL_OES} texture target. + * <p> * This can be used to access the SurfaceTexture image contents from multiple OpenGL ES * contexts. Note, however, that the image contents are only accessible from one OpenGL ES * context at a time. @@ -297,16 +298,16 @@ public class SurfaceTexture { /** * Retrieve the 4x4 texture coordinate transform matrix associated with the texture image set by - * the most recent call to updateTexImage. - * + * the most recent call to {@link #updateTexImage}. + * <p> * This transform matrix maps 2D homogeneous texture coordinates of the form (s, t, 0, 1) with s * and t in the inclusive range [0, 1] to the texture coordinate that should be used to sample * that location from the texture. Sampling the texture outside of the range of this transform * is undefined. - * + * <p> * The matrix is stored in column-major order so that it may be passed directly to OpenGL ES via - * the glLoadMatrixf or glUniformMatrix4fv functions. - * + * the {@code glLoadMatrixf} or {@code glUniformMatrix4fv} functions. + * <p> * If the underlying buffer has a crop associated with it, the transformation will also include * a slight scale to cut off a 1-texel border around the edge of the crop. This ensures that * when the texture is bilinear sampled that no texels outside of the buffer's valid region @@ -326,7 +327,7 @@ public class SurfaceTexture { /** * Retrieve the timestamp associated with the texture image set by the most recent call to - * updateTexImage. + * {@link #updateTexImage}. * * <p>This timestamp is in nanoseconds, and is normally monotonically increasing. The timestamp * should be unaffected by time-of-day adjustments. The specific meaning and zero point of the @@ -337,8 +338,8 @@ public class SurfaceTexture { * * <p>For camera sources, timestamps should be strictly monotonic. Timestamps from MediaPlayer * sources may be reset when the playback position is set. For EGL and Vulkan producers, the - * timestamp is the desired present time set with the EGL_ANDROID_presentation_time or - * VK_GOOGLE_display_timing extensions.</p> + * timestamp is the desired present time set with the {@code EGL_ANDROID_presentation_time} or + * {@code VK_GOOGLE_display_timing} extensions.</p> */ public long getTimestamp() { @@ -346,16 +347,17 @@ public class SurfaceTexture { } /** - * release() frees all the buffers and puts the SurfaceTexture into the + * {@code release()} frees all the buffers and puts the SurfaceTexture into the * 'abandoned' state. Once put in this state the SurfaceTexture can never * leave it. When in the 'abandoned' state, all methods of the - * IGraphicBufferProducer interface will fail with the NO_INIT error. - * + * {@code IGraphicBufferProducer} interface will fail with the {@code NO_INIT} + * error. + * <p> * Note that while calling this method causes all the buffers to be freed * from the perspective of the the SurfaceTexture, if there are additional * references on the buffers (e.g. if a buffer is referenced by a client or * by OpenGL ES as a texture) then those buffer will remain allocated. - * + * <p> * Always call this method when you are done with SurfaceTexture. Failing * to do so may delay resource deallocation for a significant amount of * time. @@ -367,7 +369,7 @@ public class SurfaceTexture { } /** - * Returns true if the SurfaceTexture was released. + * Returns {@code true} if the SurfaceTexture was released. * * @see #release() */ @@ -400,7 +402,7 @@ public class SurfaceTexture { } /** - * Returns true if the SurfaceTexture is single-buffered + * Returns {@code true} if the SurfaceTexture is single-buffered. * @hide */ public boolean isSingleBuffered() { diff --git a/identity/java/android/security/identity/IdentityCredential.java b/identity/java/android/security/identity/IdentityCredential.java index 1db2f6357308..b351b3d77430 100644 --- a/identity/java/android/security/identity/IdentityCredential.java +++ b/identity/java/android/security/identity/IdentityCredential.java @@ -95,9 +95,7 @@ public abstract class IdentityCredential { /** * Sets whether to allow using an authentication key which use count has been exceeded if no * other key is available. This must be called prior to calling - * {@link #getEntries(byte[], Map, byte[], byte[])} or using a - * {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} which references this - * object. + * {@link #getEntries(byte[], Map, byte[], byte[])}. * * By default this is set to true. * @@ -123,13 +121,14 @@ public abstract class IdentityCredential { * entries. * * <p>It is the responsibility of the calling application to know if authentication is needed - * and use e.g. {@link android.hardware.biometrics.BiometricPrompt}) to make the user + * and use e.g. {@link android.hardware.biometrics.BiometricPrompt} to make the user * authenticate using a {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} which * references this object. If needed, this must be done before calling * {@link #getEntries(byte[], Map, byte[], byte[])}. * - * <p>If this method returns successfully (i.e. without throwing an exception), it must not be - * called again on this instance. + * <p>It is permissible to call this method multiple times using the same instance but if this + * is done, the {@code sessionTranscript} parameter must be identical for each call. If this is + * not the case, the {@link SessionTranscriptMismatchException} exception is thrown. * * <p>If not {@code null} the {@code requestMessage} parameter must contain data for the request * from the verifier. The content can be defined in the way appropriate for the credential, byt @@ -141,6 +140,9 @@ public abstract class IdentityCredential { * the example below.</li> * </ul> * + * <p>If these requirements are not met the {@link InvalidRequestMessageException} exception + * is thrown. + * * <p>Here's an example of CBOR which conforms to this requirement: * <pre> * ItemsRequest = { @@ -149,6 +151,8 @@ public abstract class IdentityCredential { * ? "RequestInfo" : {* tstr => any} ; Additional info the reader wants to provide * } * + * DocType = tstr + * * NameSpaces = { * + NameSpace => DataElements ; Requested data elements for each NameSpace * } @@ -172,16 +176,18 @@ public abstract class IdentityCredential { * EReaderKeyBytes * ] * - * DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement) - * EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub) + * DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement) ; Bytes of DeviceEngagement + * EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub) ; Bytes of EReaderKey.pub + * + * EReaderKey.Pub = COSE_Key ; Ephemeral public key provided by reader * </pre> * - * <p>If the SessionTranscript is not empty, a COSE_Key structure for the public part - * of the key-pair previously generated by {@link #createEphemeralKeyPair()} must appear - * somewhere in {@code DeviceEngagement} and the X and Y coordinates must both be present + * <p>where a {@code COSE_Key} structure for the public part of the key-pair previously + * generated by {@link #createEphemeralKeyPair()} must appear somewhere in + * {@code DeviceEngagement} and the X and Y coordinates must both be present * in uncompressed form. * - * <p>If {@code readerAuth} is not {@code null} it must be the bytes of a COSE_Sign1 + * <p>If {@code readerAuth} is not {@code null} it must be the bytes of a {@code COSE_Sign1} * structure as defined in RFC 8152. For the payload nil shall be used and the * detached payload is the ReaderAuthentication CBOR described below. * <pre> @@ -194,20 +200,23 @@ public abstract class IdentityCredential { * ItemsRequestBytes = #6.24(bstr .cbor ItemsRequest) ; Bytes of ItemsRequest * </pre> * - * <p>The public key corresponding to the key used to made signature, can be - * found in the {@code x5chain} unprotected header element of the COSE_Sign1 - * structure (as as described in 'draft-ietf-cose-x509-04'). There will be at - * least one certificate in said element and there may be more (and if so, + * <p>where {@code ItemsRequestBytes} are the bytes in the {@code requestMessage} parameter. + * + * <p>The public key corresponding to the key used to make the signature, can be found in the + * {@code x5chain} unprotected header element of the {@code COSE_Sign1} structure (as as + * described in + * <a href="https://tools.ietf.org/html/draft-ietf-cose-x509-04">draft-ietf-cose-x509-04</a>). + * There will be at least one certificate in said element and there may be more (and if so, * each certificate must be signed by its successor). * - * <p>Data elements protected by reader authentication is returned if, and only if, they are + * <p>Data elements protected by reader authentication are returned if, and only if, they are * mentioned in {@code requestMessage}, {@code requestMessage} is signed by the top-most - * certificate in {@code readerCertificateChain}, and the data element is configured - * with an {@link AccessControlProfile} with a {@link X509Certificate} in - * {@code readerCertificateChain}. + * certificate in the reader's certificate chain, and the data element is configured + * with an {@link AccessControlProfile} configured with an X.509 certificate which appears + * in the certificate chain. * * <p>Note that only items referenced in {@code entriesToRequest} are returned - the - * {@code requestMessage} parameter is only used to for enforcing reader authentication. + * {@code requestMessage} parameter is used only for enforcing reader authentication. * * <p>The reason for having {@code requestMessage} and {@code entriesToRequest} as separate * parameters is that the former represents a request from the remote verifier device @@ -219,13 +228,12 @@ public abstract class IdentityCredential { * @param entriesToRequest The entries to request, organized as a map of namespace * names with each value being a collection of data elements * in the given namespace. - * @param readerSignature COSE_Sign1 structure as described above or {@code null} - * if reader authentication is not being used. + * @param readerSignature A {@code COSE_Sign1} structure as described above or + * {@code null} if reader authentication is not being used. * @return A {@link ResultData} object containing entry data organized by namespace and a * cryptographically authenticated representation of the same data. * @throws SessionTranscriptMismatchException Thrown when trying use multiple different - * session transcripts in the same presentation - * session. + * session transcripts. * @throws NoAuthenticationKeyAvailableException if authentication keys were never * provisioned, the method * {@link #setAvailableAuthenticationKeys(int, int)} @@ -255,8 +263,8 @@ public abstract class IdentityCredential { * Sets the number of dynamic authentication keys the {@code IdentityCredential} will maintain, * and the number of times each should be used. * - * <p>{@code IdentityCredential}s will select the least-used dynamic authentication key each - * time {@link #getEntries(byte[], Map, byte[], byte[])} is called. {@code IdentityCredential}s + * <p>The Identity Credential system will select the least-used dynamic authentication key each + * time {@link #getEntries(byte[], Map, byte[], byte[])} is called. Identity Credentials * for which this method has not been called behave as though it had been called wit * {@code keyCount} 0 and {@code maxUsesPerKey} 1. * @@ -274,9 +282,10 @@ public abstract class IdentityCredential { * <p>When there aren't enough certified dynamic authentication keys, either because the key * count has been increased or because one or more keys have reached their usage count, this * method will generate replacement keys and certificates and return them for issuer - * certification. The issuer certificates and associated static authentication data must then - * be provided back to the {@code IdentityCredential} using - * {@link #storeStaticAuthenticationData(X509Certificate, byte[])}. + * certification. The issuer certificates and associated static authentication data must then + * be provided back to the Identity Credential using + * {@link #storeStaticAuthenticationData(X509Certificate, byte[])}. The private part of + * each authentication key never leaves secure hardware. * * <p>Each X.509 certificate is signed by CredentialKey. The certificate chain for CredentialKey * can be obtained using the {@link #getCredentialKeyCertificateChain()} method. diff --git a/identity/java/android/security/identity/IdentityCredentialStore.java b/identity/java/android/security/identity/IdentityCredentialStore.java index a1dfc77adb29..4f834d2b87b5 100644 --- a/identity/java/android/security/identity/IdentityCredentialStore.java +++ b/identity/java/android/security/identity/IdentityCredentialStore.java @@ -78,17 +78,21 @@ public abstract class IdentityCredentialStore { /** * Specifies that the cipher suite that will be used to secure communications between the reader - * is: + * and the prover is using the following primitives * * <ul> - * <li>ECDHE with HKDF-SHA-256 for key agreement.</li> - * <li>AES-256 with GCM block mode for authenticated encryption (nonces are incremented by one - * for every message).</li> - * <li>ECDSA with SHA-256 for signing (used for signing session transcripts to defeat - * man-in-the-middle attacks), signing keys are not ephemeral. See {@link IdentityCredential} - * for details on reader and prover signing keys.</li> + * <li>ECKA-DH (Elliptic Curve Key Agreement Algorithm - Diffie-Hellman, see BSI TR-03111).</li> + * + * <li>HKDF-SHA-256 (see RFC 5869).</li> + * + * <li>AES-256-GCM (see NIST SP 800-38D).</li> + * + * <li>HMAC-SHA-256 (see RFC 2104).</li> * </ul> * + * <p>The exact way these primitives are combined to derive the session key is specified in + * section 9.2.1.4 of ISO/IEC 18013-5 (see description of cipher suite '1').<p> + * * <p> * At present this is the only supported cipher suite. */ @@ -135,9 +139,20 @@ public abstract class IdentityCredentialStore { /** * Creates a new credential. * + * <p>When a credential is created, a cryptographic key-pair - CredentialKey - is created which + * is used to authenticate the store to the Issuing Authority. The private part of this + * key-pair never leaves secure hardware and the public part can be obtained using + * {@link WritableIdentityCredential#getCredentialKeyCertificateChain(byte[])} on the + * returned object. + * + * <p>In addition, all of the Credential data content is imported and a certificate for the + * CredentialKey and a signature produced with the CredentialKey are created. These latter + * values may be checked by an issuing authority to verify that the data was imported into + * secure hardware and that it was imported unmodified. + * * @param credentialName The name used to identify the credential. * @param docType The document type for the credential. - * @return A @{link WritableIdentityCredential} that can be used to create a new credential. + * @return A {@link WritableIdentityCredential} that can be used to create a new credential. * @throws AlreadyPersonalizedException if a credential with the given name already exists. * @throws DocTypeNotSupportedException if the given document type isn't supported by the store. */ @@ -148,6 +163,10 @@ public abstract class IdentityCredentialStore { /** * Retrieve a named credential. * + * <p>The cipher suite used to communicate with the remote verifier must also be specified. + * Currently only a single cipher-suite is supported. Support for other cipher suites may be + * added in a future version of this API. + * * @param credentialName the name of the credential to retrieve. * @param cipherSuite the cipher suite to use for communicating with the verifier. * @return The named credential, or null if not found. diff --git a/identity/java/android/security/identity/ResultData.java b/identity/java/android/security/identity/ResultData.java index 13552d619e05..37de2c4a50ea 100644 --- a/identity/java/android/security/identity/ResultData.java +++ b/identity/java/android/security/identity/ResultData.java @@ -34,23 +34,23 @@ public abstract class ResultData { /** Value was successfully retrieved. */ public static final int STATUS_OK = 0; - /** Requested entry does not exist. */ + /** The entry does not exist. */ public static final int STATUS_NO_SUCH_ENTRY = 1; - /** Requested entry was not requested. */ + /** The entry was not requested. */ public static final int STATUS_NOT_REQUESTED = 2; - /** Requested entry wasn't in the request message. */ + /** The entry wasn't in the request message. */ public static final int STATUS_NOT_IN_REQUEST_MESSAGE = 3; - /** The requested entry was not retrieved because user authentication wasn't performed. */ + /** The entry was not retrieved because user authentication failed. */ public static final int STATUS_USER_AUTHENTICATION_FAILED = 4; - /** The requested entry was not retrieved because reader authentication wasn't performed. */ + /** The entry was not retrieved because reader authentication failed. */ public static final int STATUS_READER_AUTHENTICATION_FAILED = 5; /** - * The requested entry was not retrieved because it was configured without any access + * The entry was not retrieved because it was configured without any access * control profile. */ public static final int STATUS_NO_ACCESS_CONTROL_PROFILES = 6; @@ -88,11 +88,10 @@ public abstract class ResultData { * * DeviceEngagementBytes = #6.24(bstr .cbor DeviceEngagement) * EReaderKeyBytes = #6.24(bstr .cbor EReaderKey.Pub) - * * DeviceNameSpacesBytes = #6.24(bstr .cbor DeviceNameSpaces) * </pre> * - * where + * <p>where * * <pre> * DeviceNameSpaces = { @@ -116,15 +115,16 @@ public abstract class ResultData { public abstract @NonNull byte[] getAuthenticatedData(); /** - * Returns a message authentication code over the data returned by - * {@link #getAuthenticatedData}, to prove to the reader that the data is from a trusted - * credential. + * Returns a message authentication code over the {@code DeviceAuthentication} CBOR + * specified in {@link #getAuthenticatedData()}, to prove to the reader that the data + * is from a trusted credential. * * <p>The MAC proves to the reader that the data is from a trusted credential. This code is * produced by using the key agreement and key derivation function from the ciphersuite * with the authentication private key and the reader ephemeral public key to compute a * shared message authentication code (MAC) key, then using the MAC function from the - * ciphersuite to compute a MAC of the authenticated data. + * ciphersuite to compute a MAC of the authenticated data. See section 9.2.3.5 of + * ISO/IEC 18013-5 for details of this operation. * * <p>If the {@code sessionTranscript} parameter passed to * {@link IdentityCredential#getEntries(byte[], Map, byte[], byte[])} was {@code null} @@ -157,7 +157,7 @@ public abstract class ResultData { /** * Get the names of all entries. * - * This includes the name of entries that wasn't successfully retrieved. + * <p>This includes the name of entries that wasn't successfully retrieved. * * @param namespaceName the namespace name to get entries for. * @return A collection of names or {@code null} if there are no entries for the given @@ -168,7 +168,7 @@ public abstract class ResultData { /** * Get the names of all entries that was successfully retrieved. * - * This only return entries for which {@link #getStatus(String, String)} will return + * <p>This only return entries for which {@link #getStatus(String, String)} will return * {@link #STATUS_OK}. * * @param namespaceName the namespace name to get entries for. @@ -181,16 +181,15 @@ public abstract class ResultData { /** * Gets the status of an entry. * - * This returns {@link #STATUS_OK} if the value was retrieved, {@link #STATUS_NO_SUCH_ENTRY} + * <p>This returns {@link #STATUS_OK} if the value was retrieved, {@link #STATUS_NO_SUCH_ENTRY} * if the given entry wasn't retrieved, {@link #STATUS_NOT_REQUESTED} if it wasn't requested, * {@link #STATUS_NOT_IN_REQUEST_MESSAGE} if the request message was set but the entry wasn't - * present in the request message, - * {@link #STATUS_USER_AUTHENTICATION_FAILED} if the value + * present in the request message, {@link #STATUS_USER_AUTHENTICATION_FAILED} if the value * wasn't retrieved because the necessary user authentication wasn't performed, - * {@link #STATUS_READER_AUTHENTICATION_FAILED} if the supplied reader certificate chain - * didn't match the set of certificates the entry was provisioned with, or - * {@link #STATUS_NO_ACCESS_CONTROL_PROFILES} if the entry was configured without any - * access control profiles. + * {@link #STATUS_READER_AUTHENTICATION_FAILED} if the supplied reader certificate chain didn't + * match the set of certificates the entry was provisioned with, or + * {@link #STATUS_NO_ACCESS_CONTROL_PROFILES} if the entry was configured without any access + * control profiles. * * @param namespaceName the namespace name of the entry. * @param name the name of the entry to get the value for. @@ -201,7 +200,7 @@ public abstract class ResultData { /** * Gets the raw CBOR data for the value of an entry. * - * This should only be called on an entry for which the {@link #getStatus(String, String)} + * <p>This should only be called on an entry for which the {@link #getStatus(String, String)} * method returns {@link #STATUS_OK}. * * @param namespaceName the namespace name of the entry. diff --git a/identity/java/android/security/identity/WritableIdentityCredential.java b/identity/java/android/security/identity/WritableIdentityCredential.java index e2a389bfd4da..c7aa32855abc 100644 --- a/identity/java/android/security/identity/WritableIdentityCredential.java +++ b/identity/java/android/security/identity/WritableIdentityCredential.java @@ -41,15 +41,16 @@ public abstract class WritableIdentityCredential { * <a href="https://source.android.com/security/keystore/attestation">Android Keystore</a> * attestation extension which describes the key and the security hardware in which it lives. * - * <p>Additionally, the attestation extension will contain the tag TODO_IC_KEY which indicates - * it is an Identity Credential key (which can only sign/MAC very specific messages) and not - * an Android Keystore key (which can be used to sign/MAC anything). + * <p>Additionally, the attestation extension will contain the tag Tag::IDENTITY_CREDENTIAL_KEY + * which indicates it is an Identity Credential key (which can only sign/MAC very specific + * messages) and not an Android Keystore key (which can be used to sign/MAC anything). * * <p>The issuer <b>MUST</b> carefully examine this certificate chain including (but not - * limited to) checking that the root certificate is well-known, the tag TODO_IC_KEY is - * present, the passed in challenge is present, the device has verified boot enabled, that each - * certificate in the chain is signed by its successor, that none of the certificates have been - * revoked and so on. + * limited to) checking that the root certificate is well-known, the tag + * Tag::IDENTITY_CREDENTIAL_KEY present, the passed in challenge is present, the tag + * Tag::ATTESTATION_APPLICATION_ID is set to the expected Android application, the device + * has verified boot enabled, each certificate in the chain is signed by its successor, + * none of the certificates have been revoked, and so on. * * <p>It is not strictly necessary to use this method to provision a credential if the issuing * authority doesn't care about the nature of the security hardware. If called, however, this diff --git a/media/java/android/media/AudioPortEventHandler.java b/media/java/android/media/AudioPortEventHandler.java index 14249cbe8945..8e8dfaf24b9a 100644 --- a/media/java/android/media/AudioPortEventHandler.java +++ b/media/java/android/media/AudioPortEventHandler.java @@ -78,7 +78,8 @@ class AudioPortEventHandler { listeners.add((AudioManager.OnAudioPortUpdateListener)msg.obj); } } else { - listeners = mListeners; + listeners = (ArrayList<AudioManager.OnAudioPortUpdateListener>) + mListeners.clone(); } } // reset audio port cache if the event corresponds to a change coming diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl index dc06153ffa9f..a8b82ba401eb 100644 --- a/media/java/android/media/IMediaRouter2.aidl +++ b/media/java/android/media/IMediaRouter2.aidl @@ -24,11 +24,15 @@ import android.os.Bundle; * @hide */ oneway interface IMediaRouter2 { - void notifyRestoreRoute(); void notifyRoutesAdded(in List<MediaRoute2Info> routes); void notifyRoutesRemoved(in List<MediaRoute2Info> routes); void notifyRoutesChanged(in List<MediaRoute2Info> routes); void notifySessionCreated(int requestId, in @nullable RoutingSessionInfo sessionInfo); void notifySessionInfoChanged(in RoutingSessionInfo sessionInfo); void notifySessionReleased(in RoutingSessionInfo sessionInfo); + /** + * Gets hints of the new session for the given route. + * Call MediaRouterService#notifySessionHintsForCreatingSession to pass the result. + */ + void getSessionHintsForCreatingSession(long uniqueRequestId, in MediaRoute2Info route); } diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index 0d87736cfd47..52bac671cc6f 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -59,6 +59,8 @@ interface IMediaRouterService { void requestCreateSessionWithRouter2(IMediaRouter2 router, int requestId, in MediaRoute2Info route, in @nullable Bundle sessionHints); + void notifySessionHintsForCreatingSession(IMediaRouter2 router, long uniqueRequestId, + in MediaRoute2Info route, in @nullable Bundle sessionHints); void selectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route); void deselectRouteWithRouter2(IMediaRouter2 router, String sessionId, in MediaRoute2Info route); void transferToRouteWithRouter2(IMediaRouter2 router, String sessionId, diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index 97d32d88c7f2..c652628eb425 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -388,7 +388,7 @@ public final class MediaCas implements AutoCloseable { @Override public void onReclaimResources() { synchronized (mSessionMap) { - mSessionMap.forEach((casSession, sessionResourceId) -> casSession.close()); + mSessionMap.forEach((casSession, sessionResourceHandle) -> casSession.close()); } mEventHandler.sendMessage(mEventHandler.obtainMessage( EventHandler.MSG_CAS_RESOURCE_LOST)); @@ -868,7 +868,7 @@ public final class MediaCas implements AutoCloseable { } } - private int getSessionResourceId() throws MediaCasException { + private int getSessionResourceHandle() throws MediaCasException { validateInternalStates(); int[] sessionResourceHandle = new int[1]; @@ -881,14 +881,14 @@ public final class MediaCas implements AutoCloseable { "insufficient resource to Open Session"); } } - return sessionResourceHandle[0]; + return sessionResourceHandle[0]; } - private void addSessionToResourceMap(Session session, int sessionResourceId) { + private void addSessionToResourceMap(Session session, int sessionResourceHandle) { - if (sessionResourceId != -1) { + if (sessionResourceHandle != TunerResourceManager.INVALID_RESOURCE_HANDLE) { synchronized (mSessionMap) { - mSessionMap.put(session, sessionResourceId); + mSessionMap.put(session, sessionResourceHandle); } } } @@ -918,13 +918,13 @@ public final class MediaCas implements AutoCloseable { * @throws MediaCasStateException for CAS-specific state exceptions. */ public Session openSession() throws MediaCasException { - int sessionResourceId = getSessionResourceId(); + int sessionResourceHandle = getSessionResourceHandle(); try { OpenSessionCallback cb = new OpenSessionCallback(); mICas.openSession(cb); MediaCasException.throwExceptionIfNeeded(cb.mStatus); - addSessionToResourceMap(cb.mSession, sessionResourceId); + addSessionToResourceMap(cb.mSession, sessionResourceHandle); return cb.mSession; } catch (RemoteException e) { cleanupAndRethrowIllegalState(); @@ -952,7 +952,7 @@ public final class MediaCas implements AutoCloseable { @Nullable public Session openSession(@SessionUsage int sessionUsage, @ScramblingMode int scramblingMode) throws MediaCasException { - int sessionResourceId = getSessionResourceId(); + int sessionResourceHandle = getSessionResourceHandle(); if (mICasV12 == null) { Log.d(TAG, "Open Session with scrambling mode is only supported by cas@1.2+ interface"); @@ -963,7 +963,7 @@ public final class MediaCas implements AutoCloseable { OpenSession_1_2_Callback cb = new OpenSession_1_2_Callback(); mICasV12.openSession_1_2(sessionUsage, scramblingMode, cb); MediaCasException.throwExceptionIfNeeded(cb.mStatus); - addSessionToResourceMap(cb.mSession, sessionResourceId); + addSessionToResourceMap(cb.mSession, sessionResourceHandle); return cb.mSession; } catch (RemoteException e) { cleanupAndRethrowIllegalState(); diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index cbf2364b50a4..1bfa9991e5d2 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -1312,7 +1312,7 @@ public final class MediaFormat { } /** - * Returns the value of an long key, or the default value if the key is missing. + * Returns the value of a long key, or the default value if the key is missing. * * @return defaultValue if the key does not exist or the stored value for the key is null * @throws ClassCastException if the stored value for the key is int, float, ByteBuffer or @@ -1340,19 +1340,15 @@ public final class MediaFormat { } /** - * Returns the value of an float key, or the default value if the key is missing. + * Returns the value of a float key, or the default value if the key is missing. * * @return defaultValue if the key does not exist or the stored value for the key is null * @throws ClassCastException if the stored value for the key is int, long, ByteBuffer or * String */ public final float getFloat(@NonNull String name, float defaultValue) { - try { - return getFloat(name); - } catch (NullPointerException e) { - /* no such field or field is null */ - return defaultValue; - } + Object value = mMap.get(name); + return value != null ? (float) value : defaultValue; } /** @@ -1366,7 +1362,7 @@ public final class MediaFormat { } /** - * Returns the value of an string key, or the default value if the key is missing. + * Returns the value of a string key, or the default value if the key is missing. * * @return defaultValue if the key does not exist or the stored value for the key is null * @throws ClassCastException if the stored value for the key is int, long, float or ByteBuffer diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index bd8fb9602656..0ea962493164 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -237,9 +237,9 @@ public final class MediaRouter2 { } catch (RemoteException ex) { Log.e(TAG, "Unable to unregister media router.", ex); } + mStub = null; } mShouldUpdateRoutes = true; - mStub = null; } } @@ -690,6 +690,31 @@ public final class MediaRouter2 { matchingController.releaseInternal(/* shouldReleaseSession= */ false); } + void onGetControllerHintsForCreatingSessionOnHandler(long uniqueRequestId, + MediaRoute2Info route) { + OnGetControllerHintsListener listener = mOnGetControllerHintsListener; + Bundle controllerHints = null; + if (listener != null) { + controllerHints = listener.onGetControllerHints(route); + if (controllerHints != null) { + controllerHints = new Bundle(controllerHints); + } + } + + MediaRouter2Stub stub; + synchronized (sRouterLock) { + stub = mStub; + } + if (stub != null) { + try { + mMediaRouterService.notifySessionHintsForCreatingSession( + stub, uniqueRequestId, route, controllerHints); + } catch (RemoteException ex) { + Log.e(TAG, "getSessionHintsOnHandler: Unable to request.", ex); + } + } + } + private List<MediaRoute2Info> filterRoutes(List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryRequest) { return routes.stream() @@ -820,13 +845,14 @@ public final class MediaRouter2 { */ public interface OnGetControllerHintsListener { /** - * Called when the {@link MediaRouter2} is about to request - * the media route provider service to create a controller with the given route. + * Called when the {@link MediaRouter2} or the system is about to request + * a media route provider service to create a controller with the given route. * The {@link Bundle} returned here will be sent to media route provider service as a hint. * <p> - * To send hints when creating the controller, set the listener before calling - * {@link #transferTo(MediaRoute2Info)}. The method will be called - * on the same thread which calls {@link #transferTo(MediaRoute2Info)}. + * Since controller creation can be requested by the {@link MediaRouter2} and the system, + * set the listener as soon as possible after acquiring {@link MediaRouter2} instance. + * The method will be called on the same thread that calls + * {@link #transferTo(MediaRoute2Info)} or the main thread if it is requested by the system. * * @param route The route to create controller with * @return An optional bundle of app-specific arguments to send to the provider, @@ -1378,9 +1404,6 @@ public final class MediaRouter2 { class MediaRouter2Stub extends IMediaRouter2.Stub { @Override - public void notifyRestoreRoute() throws RemoteException {} - - @Override public void notifyRoutesAdded(List<MediaRoute2Info> routes) { mHandler.sendMessage(obtainMessage(MediaRouter2::addRoutesOnHandler, MediaRouter2.this, routes)); @@ -1415,5 +1438,13 @@ public final class MediaRouter2 { mHandler.sendMessage(obtainMessage(MediaRouter2::releaseControllerOnHandler, MediaRouter2.this, sessionInfo)); } + + @Override + public void getSessionHintsForCreatingSession(long uniqueRequestId, + @NonNull MediaRoute2Info route) { + mHandler.sendMessage(obtainMessage( + MediaRouter2::onGetControllerHintsForCreatingSessionOnHandler, + MediaRouter2.this, uniqueRequestId, route)); + } } } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index b694fd059bfa..3b570b60ff24 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -147,14 +147,16 @@ public final class MediaRouter2Manager { } synchronized (sLock) { - if (mCallbackRecords.size() == 0 && mClient != null) { - try { - mMediaRouterService.unregisterManager(mClient); - } catch (RemoteException ex) { - Log.e(TAG, "Unable to unregister media router manager", ex); + if (mCallbackRecords.size() == 0) { + if (mClient != null) { + try { + mMediaRouterService.unregisterManager(mClient); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to unregister media router manager", ex); + } + mClient = null; } - //TODO: clear mRoutes? - mClient = null; + mRoutes.clear(); mPreferredFeaturesMap.clear(); } } diff --git a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java index adf4d3dcfa09..022cfeeb4e43 100644 --- a/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java +++ b/media/java/android/media/audiopolicy/AudioVolumeGroupChangeHandler.java @@ -80,7 +80,8 @@ public class AudioVolumeGroupChangeHandler { (AudioManager.VolumeGroupCallback) msg.obj); } } else { - listeners = mListeners; + listeners = (ArrayList<AudioManager.VolumeGroupCallback>) + mListeners.clone(); } } if (listeners.isEmpty()) { diff --git a/media/java/android/media/tv/ITvRemoteServiceInput.aidl b/media/java/android/media/tv/ITvRemoteServiceInput.aidl index a0b6c9bfc8d8..0e6563a1ab13 100644 --- a/media/java/android/media/tv/ITvRemoteServiceInput.aidl +++ b/media/java/android/media/tv/ITvRemoteServiceInput.aidl @@ -39,4 +39,10 @@ oneway interface ITvRemoteServiceInput { void sendPointerUp(IBinder token, int pointerId); @UnsupportedAppUsage void sendPointerSync(IBinder token); -}
\ No newline at end of file + + // API specific to gamepads. Close gamepads with closeInputBridge + void openGamepadBridge(IBinder token, String name); + void sendGamepadKeyDown(IBinder token, int keyCode); + void sendGamepadKeyUp(IBinder token, int keyCode); + void sendGamepadAxisValue(IBinder token, int axis, float value); +} diff --git a/media/java/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl b/media/java/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl index 7077cd1b76a0..487b444eb627 100644 --- a/media/java/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl +++ b/media/java/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl @@ -218,11 +218,11 @@ interface ITunerResourceManager { * <p><strong>Note:</strong> {@link #updateCasInfo(int, int)} must be called before this request. * * @param request {@link CasSessionRequest} information of the current request. - * @param sessionResourceId a one-element array to return the granted cas session id. + * @param casSessionHandle a one-element array to return the granted cas session handle. * * @return true if there is CAS session granted. */ - boolean requestCasSession(in CasSessionRequest request, out int[] sessionResourceId); + boolean requestCasSession(in CasSessionRequest request, out int[] casSessionHandle); /* * This API is used by the Tuner framework to request an available Lnb from the TunerHAL. @@ -276,7 +276,7 @@ interface ITunerResourceManager { * * <p>Client must call this whenever it releases a descrambler. * - * @param demuxHandle the handle of the released Tuner Descrambler. + * @param descramblerHandle the handle of the released Tuner Descrambler. * @param clientId the id of the client that is releasing the descrambler. */ void releaseDescrambler(in int descramblerHandle, int clientId); @@ -288,10 +288,10 @@ interface ITunerResourceManager { * * <p><strong>Note:</strong> {@link #updateCasInfo(int, int)} must be called before this release. * - * @param sessionResourceId the id of the released CAS session. + * @param casSessionHandle the handle of the released CAS session. * @param clientId the id of the client that is releasing the cas session. */ - void releaseCasSession(in int sessionResourceId, int clientId); + void releaseCasSession(in int casSessionHandle, int clientId); /* * Notifies the TRM that the Lnb with the given handle was released. diff --git a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java index b4dcc5d8b3df..be102d8acc10 100644 --- a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java +++ b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java @@ -362,17 +362,16 @@ public class TunerResourceManager { * request. * * @param request {@link CasSessionRequest} information of the current request. - * @param sessionResourceId a one-element array to return the granted cas session id. - * If no CAS granted, this will return - * {@link #INVALID_CAS_SESSION_RESOURCE_ID}. + * @param casSessionHandle a one-element array to return the granted cas session handel. + * If no CAS granted, this will return {@link #INVALID_RESOURCE_HANDLE}. * * @return true if there is CAS session granted. */ public boolean requestCasSession(@NonNull CasSessionRequest request, - @NonNull int[] sessionResourceId) { + @NonNull int[] casSessionHandle) { boolean result = false; try { - result = mService.requestCasSession(request, sessionResourceId); + result = mService.requestCasSession(request, casSessionHandle); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -471,12 +470,12 @@ public class TunerResourceManager { * <p><strong>Note:</strong> {@link #updateCasInfo(int, int)} must be called before this * release. * - * @param sessionResourceId the id of the released CAS session. + * @param casSessionHandle the handle of the released CAS session. * @param clientId the id of the client that is releasing the cas session. */ - public void releaseCasSession(int sessionResourceId, int clientId) { + public void releaseCasSession(int casSessionHandle, int clientId) { try { - mService.releaseCasSession(sessionResourceId, clientId); + mService.releaseCasSession(casSessionHandle, clientId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java index 0bf0f97d2c5e..b97ac26bb915 100644 --- a/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java +++ b/media/lib/tvremote/java/com/android/media/tv/remoteprovider/TvRemoteProvider.java @@ -16,6 +16,8 @@ package com.android.media.tv.remoteprovider; +import android.annotation.FloatRange; +import android.annotation.NonNull; import android.content.Context; import android.media.tv.ITvRemoteProvider; import android.media.tv.ITvRemoteServiceInput; @@ -24,6 +26,7 @@ import android.os.RemoteException; import android.util.Log; import java.util.LinkedList; +import java.util.Objects; /** * Base class for emote providers implemented in unbundled service. @@ -124,27 +127,75 @@ public abstract class TvRemoteProvider { * @param maxPointers Maximum supported pointers * @throws RuntimeException */ - public void openRemoteInputBridge(IBinder token, String name, int width, int height, - int maxPointers) throws RuntimeException { + public void openRemoteInputBridge( + IBinder token, String name, int width, int height, int maxPointers) + throws RuntimeException { + final IBinder finalToken = Objects.requireNonNull(token); + final String finalName = Objects.requireNonNull(name); + synchronized (mOpenBridgeRunnables) { if (mRemoteServiceInput == null) { - Log.d(TAG, "Delaying openRemoteInputBridge() for " + name); + Log.d(TAG, "Delaying openRemoteInputBridge() for " + finalName); mOpenBridgeRunnables.add(() -> { try { mRemoteServiceInput.openInputBridge( - token, name, width, height, maxPointers); - Log.d(TAG, "Delayed openRemoteInputBridge() for " + name + ": success"); + finalToken, finalName, width, height, maxPointers); + Log.d(TAG, "Delayed openRemoteInputBridge() for " + finalName + + ": success"); + } catch (RemoteException re) { + Log.e(TAG, "Delayed openRemoteInputBridge() for " + finalName + + ": failure", re); + } + }); + return; + } + } + try { + mRemoteServiceInput.openInputBridge(finalToken, finalName, width, height, maxPointers); + Log.d(TAG, "openRemoteInputBridge() for " + finalName + ": success"); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Opens an input bridge as a gamepad device. + * Clients should pass in a token that can be used to match this request with a token that + * will be returned by {@link TvRemoteProvider#onInputBridgeConnected(IBinder token)} + * <p> + * The token should be used for subsequent calls. + * </p> + * + * @param token Identifier for this connection + * @param name Device name + * @throws RuntimeException + * + * @hide + */ + public void openGamepadBridge(@NonNull IBinder token, @NonNull String name) + throws RuntimeException { + final IBinder finalToken = Objects.requireNonNull(token); + final String finalName = Objects.requireNonNull(name); + synchronized (mOpenBridgeRunnables) { + if (mRemoteServiceInput == null) { + Log.d(TAG, "Delaying openGamepadBridge() for " + finalName); + + mOpenBridgeRunnables.add(() -> { + try { + mRemoteServiceInput.openGamepadBridge(finalToken, finalName); + Log.d(TAG, "Delayed openGamepadBridge() for " + finalName + ": success"); } catch (RemoteException re) { - Log.e(TAG, "Delayed openRemoteInputBridge() for " + name + ": failure", re); + Log.e(TAG, "Delayed openGamepadBridge() for " + finalName + ": failure", + re); } }); return; } } try { - mRemoteServiceInput.openInputBridge(token, name, width, height, maxPointers); - Log.d(TAG, "openRemoteInputBridge() for " + name + ": success"); + mRemoteServiceInput.openGamepadBridge(token, finalName); + Log.d(TAG, "openGamepadBridge() for " + finalName + ": success"); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } @@ -157,6 +208,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void closeInputBridge(IBinder token) throws RuntimeException { + Objects.requireNonNull(token); try { mRemoteServiceInput.closeInputBridge(token); } catch (RemoteException re) { @@ -173,6 +225,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void clearInputBridge(IBinder token) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "clearInputBridge() token " + token); try { mRemoteServiceInput.clearInputBridge(token); @@ -190,6 +243,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendTimestamp(IBinder token, long timestamp) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendTimestamp() token: " + token + ", timestamp: " + timestamp); try { @@ -207,6 +261,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendKeyUp(IBinder token, int keyCode) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendKeyUp() token: " + token + ", keyCode: " + keyCode); try { mRemoteServiceInput.sendKeyUp(token, keyCode); @@ -223,6 +278,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendKeyDown(IBinder token, int keyCode) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendKeyDown() token: " + token + ", keyCode: " + keyCode); try { @@ -241,6 +297,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendPointerUp(IBinder token, int pointerId) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendPointerUp() token: " + token + ", pointerId: " + pointerId); try { @@ -262,6 +319,7 @@ public abstract class TvRemoteProvider { */ public void sendPointerDown(IBinder token, int pointerId, int x, int y) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendPointerDown() token: " + token + ", pointerId: " + pointerId); try { @@ -278,6 +336,7 @@ public abstract class TvRemoteProvider { * @throws RuntimeException */ public void sendPointerSync(IBinder token) throws RuntimeException { + Objects.requireNonNull(token); if (DEBUG_KEYS) Log.d(TAG, "sendPointerSync() token: " + token); try { mRemoteServiceInput.sendPointerSync(token); @@ -286,6 +345,94 @@ public abstract class TvRemoteProvider { } } + /** + * Send a notification that a gamepad key was pressed. + * + * Supported buttons are: + * <ul> + * <li> Right-side buttons: BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y + * <li> Digital Triggers and bumpers: BUTTON_L1, BUTTON_R1, BUTTON_L2, BUTTON_R2 + * <li> Thumb buttons: BUTTON_THUMBL, BUTTON_THUMBR + * <li> DPad buttons: DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT + * <li> Gamepad buttons: BUTTON_SELECT, BUTTON_START, BUTTON_MODE + * <li> Generic buttons: BUTTON_1, BUTTON_2, ...., BUTTON16 + * <li> Assistant: ASSIST, VOICE_ASSIST + * </ul> + * + * @param token identifier for the device + * @param keyCode the gamepad key that was pressed (like BUTTON_A) + * + * @hide + */ + public void sendGamepadKeyDown(@NonNull IBinder token, int keyCode) throws RuntimeException { + Objects.requireNonNull(token); + if (DEBUG_KEYS) { + Log.d(TAG, "sendGamepadKeyDown() token: " + token); + } + + try { + mRemoteServiceInput.sendGamepadKeyDown(token, keyCode); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Send a notification that a gamepad key was released. + * + * @see sendGamepadKeyDown for supported key codes. + * + * @param token identifier for the device + * @param keyCode the gamepad key that was pressed + * + * @hide + */ + public void sendGamepadKeyUp(@NonNull IBinder token, int keyCode) throws RuntimeException { + Objects.requireNonNull(token); + if (DEBUG_KEYS) { + Log.d(TAG, "sendGamepadKeyUp() token: " + token); + } + + try { + mRemoteServiceInput.sendGamepadKeyUp(token, keyCode); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** + * Send a gamepad axis value. + * + * Supported axes: + * <li> Left Joystick: AXIS_X, AXIS_Y + * <li> Right Joystick: AXIS_Z, AXIS_RZ + * <li> Triggers: AXIS_LTRIGGER, AXIS_RTRIGGER + * <li> DPad: AXIS_HAT_X, AXIS_HAT_Y + * + * For non-trigger axes, the range of acceptable values is [-1, 1]. The trigger axes support + * values [0, 1]. + * + * @param token identifier for the device + * @param axis MotionEvent axis + * @param value the value to send + * + * @hide + */ + public void sendGamepadAxisValue( + @NonNull IBinder token, int axis, @FloatRange(from = -1.0f, to = 1.0f) float value) + throws RuntimeException { + Objects.requireNonNull(token); + if (DEBUG_KEYS) { + Log.d(TAG, "sendGamepadAxisValue() token: " + token); + } + + try { + mRemoteServiceInput.sendGamepadAxisValue(token, axis, value); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + private final class ProviderStub extends ITvRemoteProvider.Stub { @Override public void setRemoteServiceInputSink(ITvRemoteServiceInput tvServiceInput) { diff --git a/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java b/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java index c9ce56138217..e6e39390962e 100644 --- a/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java +++ b/media/lib/tvremote/tests/src/com/android/media/tv/remoteprovider/TvRemoteProviderTest.java @@ -83,4 +83,52 @@ public class TvRemoteProviderTest extends AndroidTestCase { assertTrue(tvProvider.verifyTokens()); } + + @SmallTest + public void testOpenGamepadRemoteInputBridge() throws Exception { + Binder tokenA = new Binder(); + Binder tokenB = new Binder(); + Binder tokenC = new Binder(); + + class LocalTvRemoteProvider extends TvRemoteProvider { + private final ArrayList<IBinder> mTokens = new ArrayList<IBinder>(); + + LocalTvRemoteProvider(Context context) { + super(context); + } + + @Override + public void onInputBridgeConnected(IBinder token) { + mTokens.add(token); + } + + public boolean verifyTokens() { + return mTokens.size() == 3 && mTokens.contains(tokenA) && mTokens.contains(tokenB) + && mTokens.contains(tokenC); + } + } + + LocalTvRemoteProvider tvProvider = new LocalTvRemoteProvider(getContext()); + ITvRemoteProvider binder = (ITvRemoteProvider) tvProvider.getBinder(); + + ITvRemoteServiceInput tvServiceInput = mock(ITvRemoteServiceInput.class); + doAnswer((i) -> { + binder.onInputBridgeConnected(i.getArgument(0)); + return null; + }) + .when(tvServiceInput) + .openGamepadBridge(any(), any()); + + tvProvider.openGamepadBridge(tokenA, "A"); + tvProvider.openGamepadBridge(tokenB, "B"); + binder.setRemoteServiceInputSink(tvServiceInput); + tvProvider.openGamepadBridge(tokenC, "C"); + + verify(tvServiceInput).openGamepadBridge(tokenA, "A"); + verify(tvServiceInput).openGamepadBridge(tokenB, "B"); + verify(tvServiceInput).openGamepadBridge(tokenC, "C"); + verifyNoMoreInteractions(tvServiceInput); + + assertTrue(tvProvider.verifyTokens()); + } } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java index 6ca564fb34cc..6a1e9656cf2c 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java @@ -36,6 +36,7 @@ import static com.android.mediaroutertest.StubMediaRoute2ProviderService.VOLUME_ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.content.Context; @@ -47,6 +48,7 @@ import android.media.MediaRouter2Manager; import android.media.MediaRouter2Utils; import android.media.RouteDiscoveryPreference; import android.media.RoutingSessionInfo; +import android.os.Bundle; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -73,6 +75,8 @@ public class MediaRouter2ManagerTest { private static final String TAG = "MediaRouter2ManagerTest"; private static final int WAIT_TIME_MS = 2000; private static final int TIMEOUT_MS = 5000; + private static final String TEST_KEY = "test_key"; + private static final String TEST_VALUE = "test_value"; private Context mContext; private MediaRouter2Manager mManager; @@ -160,6 +164,7 @@ public class MediaRouter2ManagerTest { }); MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2); + assertNotNull(routeToRemove); StubMediaRoute2ProviderService sInstance = StubMediaRoute2ProviderService.getInstance(); @@ -171,6 +176,52 @@ public class MediaRouter2ManagerTest { assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); } + @Test + public void testGetRoutes_removedRoute_returnsCorrectRoutes() throws Exception { + CountDownLatch addedLatch = new CountDownLatch(1); + CountDownLatch removedLatch = new CountDownLatch(1); + + RouteCallback routeCallback = new RouteCallback() { + // Used to ensure the removed route is added. + @Override + public void onRoutesAdded(List<MediaRoute2Info> routes) { + if (removedLatch.getCount() > 0) { + return; + } + addedLatch.countDown(); + } + + @Override + public void onRoutesRemoved(List<MediaRoute2Info> routes) { + removedLatch.countDown(); + } + }; + + mRouter2.registerRouteCallback(mExecutor, routeCallback, + new RouteDiscoveryPreference.Builder(FEATURES_ALL, true).build()); + mRouteCallbacks.add(routeCallback); + + Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL); + MediaRoute2Info routeToRemove = routes.get(ROUTE_ID2); + assertNotNull(routeToRemove); + + StubMediaRoute2ProviderService sInstance = + StubMediaRoute2ProviderService.getInstance(); + assertNotNull(sInstance); + sInstance.removeRoute(ROUTE_ID2); + + // Wait until the route is removed. + assertTrue(removedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + Map<String, MediaRoute2Info> newRoutes = waitAndGetRoutesWithManager(FEATURES_ALL); + assertNull(newRoutes.get(ROUTE_ID2)); + + // Revert the removal. + sInstance.addRoute(routeToRemove); + assertTrue(addedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + mRouter2.unregisterRouteCallback(routeCallback); + } + /** * Tests if we get proper routes for application that has special route feature. */ @@ -465,6 +516,56 @@ public class MediaRouter2ManagerTest { assertEquals(VOLUME_MAX, variableVolumeRoute.getVolumeMax()); } + @Test + public void testRouter2SetOnGetControllerHintsListener() throws Exception { + Map<String, MediaRoute2Info> routes = waitAndGetRoutesWithManager(FEATURES_ALL); + addRouterCallback(new RouteCallback() {}); + + MediaRoute2Info route = routes.get(ROUTE_ID1); + assertNotNull(route); + + final Bundle controllerHints = new Bundle(); + controllerHints.putString(TEST_KEY, TEST_VALUE); + final CountDownLatch hintLatch = new CountDownLatch(1); + final MediaRouter2.OnGetControllerHintsListener listener = + route1 -> { + hintLatch.countDown(); + return controllerHints; + }; + + final CountDownLatch successLatch = new CountDownLatch(1); + final CountDownLatch failureLatch = new CountDownLatch(1); + + addManagerCallback(new MediaRouter2Manager.Callback() { + @Override + public void onTransferred(RoutingSessionInfo oldSession, + RoutingSessionInfo newSession) { + assertTrue(newSession.getSelectedRoutes().contains(route.getId())); + // The StubMediaRoute2ProviderService is supposed to set control hints + // with the given controllerHints. + Bundle controlHints = newSession.getControlHints(); + assertNotNull(controlHints); + assertTrue(controlHints.containsKey(TEST_KEY)); + assertEquals(TEST_VALUE, controlHints.getString(TEST_KEY)); + + successLatch.countDown(); + } + + @Override + public void onTransferFailed(RoutingSessionInfo session, + MediaRoute2Info requestedRoute) { + failureLatch.countDown(); + } + }); + + mRouter2.setOnGetControllerHintsListener(listener); + mManager.selectRoute(mPackageName, route); + assertTrue(hintLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + assertTrue(successLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)); + + assertFalse(failureLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS)); + } + Map<String, MediaRoute2Info> waitAndGetRoutesWithManager(List<String> routeFeatures) throws Exception { CountDownLatch addedLatch = new CountDownLatch(1); @@ -475,8 +576,8 @@ public class MediaRouter2ManagerTest { MediaRouter2Manager.Callback managerCallback = new MediaRouter2Manager.Callback() { @Override public void onRoutesAdded(List<MediaRoute2Info> routes) { - for (int i = 0; i < routes.size(); i++) { - if (!routes.get(i).isSystemRoute()) { + for (MediaRoute2Info route : routes) { + if (!route.isSystemRoute()) { addedLatch.countDown(); break; } diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java index 6d46ba582ddc..4e398f26366a 100644 --- a/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java +++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/StubMediaRoute2ProviderService.java @@ -65,9 +65,9 @@ public class StubMediaRoute2ProviderService extends MediaRoute2ProviderService { public static final String ROUTE_NAME_VARIABLE_VOLUME = "Variable Volume Route"; public static final String FEATURE_SAMPLE = - "com.android.mediarouteprovider.FEATURE_SAMPLE"; + "com.android.mediaroutertest.FEATURE_SAMPLE"; public static final String FEATURE_SPECIAL = - "com.android.mediarouteprovider.FEATURE_SPECIAL"; + "com.android.mediaroutertest..FEATURE_SPECIAL"; Map<String, MediaRoute2Info> mRoutes = new HashMap<>(); Map<String, String> mRouteIdToSessionId = new HashMap<>(); diff --git a/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml b/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml index 7004fb64ba06..8b235e6f246e 100644 --- a/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml +++ b/packages/CarSystemUI/res-keyguard/layout/keyguard_password_view.xml @@ -29,8 +29,6 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" - androidprv:layout_maxWidth="@dimen/keyguard_security_width" - androidprv:layout_maxHeight="@dimen/keyguard_security_height" android:gravity="center"> <include layout="@layout/keyguard_message_area" /> diff --git a/packages/CarSystemUI/res/layout/car_qs_footer.xml b/packages/CarSystemUI/res/layout/car_qs_footer.xml deleted file mode 100644 index bf96c00e3f0d..000000000000 --- a/packages/CarSystemUI/res/layout/car_qs_footer.xml +++ /dev/null @@ -1,83 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2018 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<!-- extends RelativeLayout --> -<com.android.systemui.qs.car.CarQSFooter - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/qs_footer" - android:layout_width="match_parent" - android:layout_height="@dimen/car_qs_footer_height" - android:baselineAligned="false" - android:clickable="false" - android:clipChildren="false" - android:clipToPadding="false" - android:paddingBottom="@dimen/car_qs_footer_padding_bottom" - android:paddingTop="@dimen/car_qs_footer_padding_top" - android:paddingEnd="@dimen/car_qs_footer_padding_end" - android:paddingStart="@dimen/car_qs_footer_padding_start" - android:gravity="center_vertical"> - - <com.android.systemui.statusbar.phone.MultiUserSwitch - android:id="@+id/multi_user_switch" - android:layout_alignParentStart="true" - android:layout_centerVertical="true" - android:layout_width="@dimen/car_qs_footer_icon_width" - android:layout_height="@dimen/car_qs_footer_icon_height" - android:background="?android:attr/selectableItemBackground" - android:focusable="true"> - - <ImageView - android:id="@+id/multi_user_avatar" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:scaleType="fitCenter"/> - </com.android.systemui.statusbar.phone.MultiUserSwitch> - - <ImageView - android:id="@+id/user_switch_expand_icon" - android:layout_height="match_parent" - android:layout_width="@dimen/car_qs_footer_user_switch_icon_width" - android:layout_centerVertical="true" - android:layout_toEndOf="@+id/multi_user_switch" - android:layout_marginLeft="@dimen/car_qs_footer_user_switch_icon_margin" - android:layout_marginRight="@dimen/car_qs_footer_user_switch_icon_margin" - android:src="@drawable/car_ic_arrow_drop_up" - android:scaleType="fitCenter"> - </ImageView> - - <TextView android:id="@+id/user_name" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textSize="@dimen/car_qs_footer_user_name_text_size" - android:textColor="@color/car_qs_footer_user_name_color" - android:gravity="start|center_vertical" - android:layout_centerVertical="true" - android:layout_toEndOf="@id/user_switch_expand_icon" /> - - <com.android.systemui.statusbar.phone.SettingsButton - android:id="@+id/settings_button" - android:layout_alignParentEnd="true" - android:layout_centerVertical="true" - android:layout_width="@dimen/car_qs_footer_icon_width" - android:layout_height="@dimen/car_qs_footer_icon_height" - android:background="@drawable/ripple_drawable" - android:contentDescription="@string/accessibility_quick_settings_settings" - android:scaleType="centerCrop" - android:src="@drawable/ic_settings_16dp" - android:tint="?android:attr/colorForeground" - style="@android:style/Widget.Material.Button.Borderless" /> - -</com.android.systemui.qs.car.CarQSFooter> diff --git a/packages/CarSystemUI/res/layout/car_qs_panel.xml b/packages/CarSystemUI/res/layout/car_qs_panel.xml deleted file mode 100644 index 0c6f322ca261..000000000000 --- a/packages/CarSystemUI/res/layout/car_qs_panel.xml +++ /dev/null @@ -1,43 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2018 The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> -<LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/quick_settings_container" - android:clipChildren="false" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@color/car_qs_background_primary" - android:orientation="vertical" - android:elevation="4dp"> - - <include layout="@layout/car_status_bar_header"/> - <include layout="@layout/car_qs_footer"/> - - <RelativeLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/user_switcher_container" - android:clipChildren="false" - android:layout_width="match_parent" - android:layout_height="@dimen/car_user_switcher_container_height"> - - <com.android.systemui.car.userswitcher.UserGridRecyclerView - android:id="@+id/user_grid" - android:layout_width="match_parent" - android:layout_height="match_parent"/> - - </RelativeLayout> - -</LinearLayout> diff --git a/packages/CarSystemUI/res/layout/car_status_bar_header.xml b/packages/CarSystemUI/res/layout/car_status_bar_header.xml index 81c7108a4cb2..12c9f11b3064 100644 --- a/packages/CarSystemUI/res/layout/car_status_bar_header.xml +++ b/packages/CarSystemUI/res/layout/car_status_bar_header.xml @@ -15,7 +15,7 @@ ~ limitations under the License --> <!-- Extends LinearLayout --> -<com.android.systemui.qs.car.CarStatusBarHeader +<com.android.systemui.car.userswitcher.CarStatusBarHeader xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/header" android:layout_width="match_parent" @@ -27,4 +27,4 @@ android:layout_height="match_parent" android:layout_weight="1" /> -</com.android.systemui.qs.car.CarStatusBarHeader> +</com.android.systemui.car.userswitcher.CarStatusBarHeader> diff --git a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml index ce0d31c6cc47..a7347f22f2e5 100644 --- a/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml +++ b/packages/CarSystemUI/res/layout/car_top_navigation_bar.xml @@ -45,7 +45,7 @@ systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end" /> - <com.android.systemui.statusbar.hvac.AnimatedTemperatureView + <com.android.systemui.car.hvac.AnimatedTemperatureView android:id="@+id/lefttext" android:layout_width="wrap_content" android:layout_height="match_parent" @@ -127,7 +127,7 @@ systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end" /> - <com.android.systemui.statusbar.hvac.AnimatedTemperatureView + <com.android.systemui.car.hvac.AnimatedTemperatureView android:id="@+id/righttext" android:layout_width="wrap_content" android:layout_height="match_parent" diff --git a/packages/CarSystemUI/res/layout/car_top_navigation_bar_unprovisioned.xml b/packages/CarSystemUI/res/layout/car_top_navigation_bar_unprovisioned.xml index a71567c48eaf..aa0a8c584896 100644 --- a/packages/CarSystemUI/res/layout/car_top_navigation_bar_unprovisioned.xml +++ b/packages/CarSystemUI/res/layout/car_top_navigation_bar_unprovisioned.xml @@ -45,7 +45,7 @@ systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end" /> - <com.android.systemui.statusbar.hvac.AnimatedTemperatureView + <com.android.systemui.car.hvac.AnimatedTemperatureView android:id="@+id/lefttext" android:layout_width="wrap_content" android:layout_height="match_parent" @@ -123,7 +123,7 @@ systemui:intent="intent:#Intent;action=android.car.intent.action.TOGGLE_HVAC_CONTROLS;end" /> - <com.android.systemui.statusbar.hvac.AnimatedTemperatureView + <com.android.systemui.car.hvac.AnimatedTemperatureView android:id="@+id/righttext" android:layout_width="wrap_content" android:layout_height="match_parent" diff --git a/packages/CarSystemUI/res/values/colors.xml b/packages/CarSystemUI/res/values/colors.xml index 7972e09869d3..3e44721848a1 100644 --- a/packages/CarSystemUI/res/values/colors.xml +++ b/packages/CarSystemUI/res/values/colors.xml @@ -20,6 +20,7 @@ <color name="car_user_switcher_background_color">#000000</color> <color name="car_user_switcher_name_text_color">@*android:color/car_body1_light</color> <color name="car_user_switcher_add_user_background_color">#131313</color> + <color name="car_user_switcher_add_user_add_sign_color">@*android:color/car_body1_light</color> <color name="car_nav_icon_fill_color">#8Fffffff</color> <color name="car_nav_icon_fill_color_selected">#ffffff</color> <!-- colors for seekbar --> diff --git a/packages/CarSystemUI/res/values/colors_car.xml b/packages/CarSystemUI/res/values/colors_car.xml deleted file mode 100644 index 5f33f8f94a9a..000000000000 --- a/packages/CarSystemUI/res/values/colors_car.xml +++ /dev/null @@ -1,28 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/* - * Copyright 2018, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ ---> -<resources> - <color name="car_qs_background_primary">#263238</color> <!-- Blue Gray 900 --> - <color name="car_qs_footer_user_name_color">@*android:color/car_grey_50</color> - - <!-- colors for user switcher --> - <color name="car_user_switcher_background_color">@*android:color/car_card_dark</color> - <color name="car_user_switcher_name_text_color">@*android:color/car_body1_light</color> - <color name="car_user_switcher_add_user_background_color">@*android:color/car_dark_blue_grey_600</color> - <color name="car_user_switcher_add_user_add_sign_color">@*android:color/car_body1_light</color> -</resources> diff --git a/packages/CarSystemUI/res/values/config.xml b/packages/CarSystemUI/res/values/config.xml index e6fb501d02e1..aeff35d88e23 100644 --- a/packages/CarSystemUI/res/values/config.xml +++ b/packages/CarSystemUI/res/values/config.xml @@ -86,29 +86,29 @@ <string-array name="config_systemUIServiceComponents" translatable="false"> <item>com.android.systemui.util.NotificationChannels</item> <item>com.android.systemui.keyguard.KeyguardViewMediator</item> - <item>com.android.systemui.recents.Recents</item> +<!-- <item>com.android.systemui.recents.Recents</item>--> <item>com.android.systemui.volume.VolumeUI</item> - <item>com.android.systemui.stackdivider.Divider</item> +<!-- <item>com.android.systemui.stackdivider.Divider</item>--> <!-- <item>com.android.systemui.statusbar.phone.StatusBar</item>--> <item>com.android.systemui.usb.StorageNotification</item> <item>com.android.systemui.power.PowerUI</item> <item>com.android.systemui.media.RingtonePlayer</item> - <item>com.android.systemui.keyboard.KeyboardUI</item> - <item>com.android.systemui.pip.PipUI</item> - <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item> +<!-- <item>com.android.systemui.keyboard.KeyboardUI</item>--> +<!-- <item>com.android.systemui.pip.PipUI</item>--> +<!-- <item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item>--> <item>@string/config_systemUIVendorServiceComponent</item> <item>com.android.systemui.util.leak.GarbageMonitor$Service</item> - <item>com.android.systemui.LatencyTester</item> - <item>com.android.systemui.globalactions.GlobalActionsComponent</item> +<!-- <item>com.android.systemui.LatencyTester</item>--> +<!-- <item>com.android.systemui.globalactions.GlobalActionsComponent</item>--> <item>com.android.systemui.ScreenDecorations</item> <item>com.android.systemui.biometrics.AuthController</item> - <item>com.android.systemui.SliceBroadcastRelayHandler</item> +<!-- <item>com.android.systemui.SliceBroadcastRelayHandler</item>--> <item>com.android.systemui.SizeCompatModeActivityController</item> - <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item> +<!-- <item>com.android.systemui.statusbar.notification.InstantAppNotifier</item>--> <item>com.android.systemui.theme.ThemeOverlayController</item> <item>com.android.systemui.navigationbar.car.CarNavigationBar</item> <item>com.android.systemui.toast.ToastUI</item> - <item>com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier</item> + <item>com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier</item> <item>com.android.systemui.window.SystemUIOverlayWindowManager</item> </string-array> </resources> diff --git a/packages/CarSystemUI/res/values/dimens.xml b/packages/CarSystemUI/res/values/dimens.xml index f68d0349e6a0..9014eb15d6cf 100644 --- a/packages/CarSystemUI/res/values/dimens.xml +++ b/packages/CarSystemUI/res/values/dimens.xml @@ -22,16 +22,11 @@ <dimen name="status_bar_icon_drawing_size_dark">36dp</dimen> <dimen name="status_bar_icon_drawing_size">36dp</dimen> - <dimen name="car_qs_header_system_icons_area_height">96dp</dimen> <!-- The amount by which to scale up the status bar icons. --> <item name="status_bar_icon_scale_factor" format="float" type="dimen">1.75</item> <dimen name="car_primary_icon_size">@*android:dimen/car_primary_icon_size</dimen> - <!-- dimensions for the car user switcher --> - <dimen name="car_user_switcher_name_text_size">@dimen/car_body1_size</dimen> - <dimen name="car_user_switcher_vertical_spacing_between_users">124dp</dimen> - <!--These values represent MIN and MAX for hvac--> <item name="hvac_min_value" format="float" type="dimen">0</item> <item name="hvac_max_value" format="float" type="dimen">126</item> @@ -90,9 +85,6 @@ <!-- The width of panel holding the notification card. --> <dimen name="notification_panel_width">522dp</dimen> - <!-- The width of the quick settings panel. -1 for match_parent. --> - <dimen name="qs_panel_width">-1px</dimen> - <!-- Height of a small notification in the status bar--> <dimen name="notification_min_height">192dp</dimen> @@ -149,7 +141,19 @@ child closer so there is less wasted space. --> <dimen name="notification_children_container_margin_top">68dp</dimen> - <!-- The height of the quick settings footer that holds the user switcher, settings icon, - etc. in the car setting.--> - <dimen name="qs_footer_height">74dp</dimen> + <!-- dimensions for the car user switcher --> + <dimen name="car_user_switcher_name_text_size">@*android:dimen/car_body1_size</dimen> + <dimen name="car_user_switcher_image_avatar_size">@*android:dimen/car_large_avatar_size</dimen> + <dimen name="car_user_switcher_vertical_spacing_between_users">@*android:dimen/car_padding_5</dimen> + <dimen name="car_user_switcher_vertical_spacing_between_name_and_avatar">@*android:dimen/car_padding_4</dimen> + <dimen name="car_user_switcher_margin_top">@*android:dimen/car_padding_4</dimen> + + <dimen name="car_navigation_button_width">64dp</dimen> + <dimen name="car_navigation_bar_width">760dp</dimen> + <dimen name="car_left_navigation_bar_width">96dp</dimen> + <dimen name="car_right_navigation_bar_width">96dp</dimen> + + <dimen name="car_user_switcher_container_height">420dp</dimen> + <!-- This must be the negative of car_user_switcher_container_height for the animation. --> + <dimen name="car_user_switcher_container_anim_height">-420dp</dimen> </resources> diff --git a/packages/CarSystemUI/res/values/dimens_car.xml b/packages/CarSystemUI/res/values/dimens_car.xml deleted file mode 100644 index e7ecf7fafc9f..000000000000 --- a/packages/CarSystemUI/res/values/dimens_car.xml +++ /dev/null @@ -1,45 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - * Copyright (c) 2018, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. -*/ ---> -<resources> - <!-- dimensions for the car user switcher --> - <dimen name="car_user_switcher_name_text_size">@*android:dimen/car_body1_size</dimen> - <dimen name="car_user_switcher_image_avatar_size">@*android:dimen/car_large_avatar_size</dimen> - <dimen name="car_user_switcher_vertical_spacing_between_users">@*android:dimen/car_padding_5</dimen> - <dimen name="car_user_switcher_vertical_spacing_between_name_and_avatar">@*android:dimen/car_padding_4</dimen> - <dimen name="car_user_switcher_margin_top">@*android:dimen/car_padding_4</dimen> - - <dimen name="car_navigation_button_width">64dp</dimen> - <dimen name="car_navigation_bar_width">760dp</dimen> - <dimen name="car_left_navigation_bar_width">96dp</dimen> - <dimen name="car_right_navigation_bar_width">96dp</dimen> - - <dimen name="car_qs_footer_height">112dp</dimen> - <dimen name="car_qs_footer_padding_bottom">16dp</dimen> - <dimen name="car_qs_footer_padding_top">16dp</dimen> - <dimen name="car_qs_footer_padding_end">46dp</dimen> - <dimen name="car_qs_footer_padding_start">46dp</dimen> - <dimen name="car_qs_footer_icon_width">56dp</dimen> - <dimen name="car_qs_footer_icon_height">56dp</dimen> - <dimen name="car_qs_footer_user_switch_icon_margin">5dp</dimen> - <dimen name="car_qs_footer_user_switch_icon_width">36dp</dimen> - <dimen name="car_qs_footer_user_name_text_size">@*android:dimen/car_body2_size</dimen> - - <dimen name="car_user_switcher_container_height">420dp</dimen> - <!-- This must be the negative of car_user_switcher_container_height for the animation. --> - <dimen name="car_user_switcher_container_anim_height">-420dp</dimen> -</resources> diff --git a/packages/CarSystemUI/res/values/ids_car.xml b/packages/CarSystemUI/res/values/ids.xml index 27ed2e250d9f..27ed2e250d9f 100644 --- a/packages/CarSystemUI/res/values/ids_car.xml +++ b/packages/CarSystemUI/res/values/ids.xml diff --git a/packages/CarSystemUI/res/values/integers.xml b/packages/CarSystemUI/res/values/integers.xml index 8b87c740425f..5ae5555a8092 100644 --- a/packages/CarSystemUI/res/values/integers.xml +++ b/packages/CarSystemUI/res/values/integers.xml @@ -16,5 +16,19 @@ --> <resources> - <integer name="user_fullscreen_switcher_num_col">2</integer> + <!-- Full screen user switcher column number --> + <integer name="user_fullscreen_switcher_num_col">3</integer> + + <!--Percentage of the screen height, from the bottom, that a notification panel being + partially closed at will result in it remaining open if released--> + <integer name="notification_settle_open_percentage">20</integer> + <!--Percentage of the screen height, from the bottom, that a notification panel being peeked + at will result in remaining closed the panel if released--> + <integer name="notification_settle_close_percentage">80</integer> + + <!-- Timeout values in milliseconds for displaying volume dialog--> + <integer name="car_volume_dialog_display_normal_timeout">3000</integer> + <integer name="car_volume_dialog_display_hovering_timeout">16000</integer> + <integer name="car_volume_dialog_display_expanded_normal_timeout">6000</integer> + <integer name="car_volume_dialog_display_expanded_hovering_timeout">32000</integer> </resources> diff --git a/packages/CarSystemUI/res/values/integers_car.xml b/packages/CarSystemUI/res/values/integers_car.xml deleted file mode 100644 index db8ce9544705..000000000000 --- a/packages/CarSystemUI/res/values/integers_car.xml +++ /dev/null @@ -1,34 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (c) 2018, The Android Open Source Project - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. ---> - -<resources xmlns:android="http://schemas.android.com/apk/res/android"> - <!-- Full screen user switcher column number TODO: move to support library--> - <integer name="user_fullscreen_switcher_num_col">3</integer> - - <!--Percentage of the screen height, from the bottom, that a notification panel being - partially closed at will result in it remaining open if released--> - <integer name="notification_settle_open_percentage">20</integer> - <!--Percentage of the screen height, from the bottom, that a notification panel being peeked - at will result in remaining closed the panel if released--> - <integer name="notification_settle_close_percentage">80</integer> - - <!-- Timeout values in milliseconds for displaying volume dialog--> - <integer name="car_volume_dialog_display_normal_timeout">3000</integer> - <integer name="car_volume_dialog_display_hovering_timeout">16000</integer> - <integer name="car_volume_dialog_display_expanded_normal_timeout">6000</integer> - <integer name="car_volume_dialog_display_expanded_hovering_timeout">32000</integer> -</resources> diff --git a/packages/CarSystemUI/res/values/strings.xml b/packages/CarSystemUI/res/values/strings.xml index 9ea7ed027d34..881e746d633d 100644 --- a/packages/CarSystemUI/res/values/strings.xml +++ b/packages/CarSystemUI/res/values/strings.xml @@ -22,4 +22,16 @@ <string name="hvac_max_text">Max</string> <!-- Text for voice recognition toast. [CHAR LIMIT=60] --> <string name="voice_recognition_toast">Voice recognition now handled by connected Bluetooth device</string> + <!-- Name of Guest Profile. [CHAR LIMIT=30] --> + <string name="car_guest">Guest</string> + <!-- Title for button that starts a guest session. [CHAR LIMIT=30] --> + <string name="start_guest_session">Guest</string> + <!-- Title for button that adds a new user. [CHAR LIMIT=30] --> + <string name="car_add_user">Add User</string> + <!-- Default name of the new user created. [CHAR LIMIT=30] --> + <string name="car_new_user">New User</string> + <!-- Message to inform user that creation of new user requires that user to set up their space. [CHAR LIMIT=100] --> + <string name="user_add_user_message_setup">When you add a new user, that person needs to set up their space.</string> + <!-- Message to inform user that the newly created user will have permissions to update apps for all other users. [CHAR LIMIT=100] --> + <string name="user_add_user_message_update">Any user can update apps for all other users.</string> </resources> diff --git a/packages/CarSystemUI/res/values/strings_car.xml b/packages/CarSystemUI/res/values/strings_car.xml deleted file mode 100644 index 83e91c57ccc3..000000000000 --- a/packages/CarSystemUI/res/values/strings_car.xml +++ /dev/null @@ -1,32 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- -/** - * Copyright (c) 2018, The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ ---> -<resources> - <!-- Name of Guest Profile. [CHAR LIMIT=30] --> - <string name="car_guest">Guest</string> - <!-- Title for button that starts a guest session. [CHAR LIMIT=30] --> - <string name="start_guest_session">Guest</string> - <!-- Title for button that adds a new user. [CHAR LIMIT=30] --> - <string name="car_add_user">Add User</string> - <!-- Default name of the new user created. [CHAR LIMIT=30] --> - <string name="car_new_user">New User</string> - <!-- Message to inform user that creation of new user requires that user to set up their space. [CHAR LIMIT=100] --> - <string name="user_add_user_message_setup">When you add a new user, that person needs to set up their space.</string> - <!-- Message to inform user that the newly created user will have permissions to update apps for all other users. [CHAR LIMIT=100] --> - <string name="user_add_user_message_update">Any user can update apps for all other users.</string> -</resources> diff --git a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java index 59fa9d09c9ee..a69c92f0bd05 100644 --- a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java +++ b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java @@ -19,6 +19,7 @@ package com.android.systemui; import com.android.systemui.biometrics.AuthController; import com.android.systemui.bubbles.dagger.BubbleModule; import com.android.systemui.car.notification.CarNotificationModule; +import com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier; import com.android.systemui.globalactions.GlobalActionsComponent; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.dagger.KeyguardModule; @@ -38,7 +39,6 @@ import com.android.systemui.statusbar.tv.TvStatusBar; import com.android.systemui.theme.ThemeOverlayController; import com.android.systemui.toast.ToastUI; import com.android.systemui.util.leak.GarbageMonitor; -import com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier; import com.android.systemui.volume.VolumeUI; import com.android.systemui.window.OverlayWindowModule; import com.android.systemui.window.SystemUIOverlayWindowManager; diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/AnimatedTemperatureView.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/AnimatedTemperatureView.java index 908aaad71893..a7294317f46c 100644 --- a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/AnimatedTemperatureView.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/AnimatedTemperatureView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 The Android Open Source Project + * Copyright (C) 2020 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. @@ -11,10 +11,10 @@ * 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 + * limitations under the License. */ -package com.android.systemui.statusbar.hvac; +package com.android.systemui.car.hvac; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; @@ -35,7 +35,6 @@ import android.widget.TextSwitcher; import android.widget.TextView; import com.android.systemui.R; -import com.android.systemui.navigationbar.car.hvac.TemperatureView; /** * Simple text display of HVAC properties, It is designed to show mTemperature and is configured in diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/HvacController.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/HvacController.java index fd9c488278ba..af8ddb6a8180 100644 --- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/HvacController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/HvacController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2020 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.navigationbar.car.hvac; +package com.android.systemui.car.hvac; import static android.car.VehicleAreaType.VEHICLE_AREA_TYPE_GLOBAL; import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_DISPLAY_UNITS; diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureBackgroundAnimator.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureBackgroundAnimator.java index 3c6d623c8ff7..a4c45730a9c2 100644 --- a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureBackgroundAnimator.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureBackgroundAnimator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 The Android Open Source Project + * Copyright (C) 2020 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. @@ -11,15 +11,15 @@ * 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 + * limitations under the License. */ -package com.android.systemui.statusbar.hvac; +package com.android.systemui.car.hvac; -import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isHorizontal; -import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isLeft; -import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isTop; -import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isVertical; +import static com.android.systemui.car.hvac.AnimatedTemperatureView.isHorizontal; +import static com.android.systemui.car.hvac.AnimatedTemperatureView.isLeft; +import static com.android.systemui.car.hvac.AnimatedTemperatureView.isTop; +import static com.android.systemui.car.hvac.AnimatedTemperatureView.isVertical; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureColorStore.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureColorStore.java index a40ffaf850c5..9a7b0b9819c5 100644 --- a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureColorStore.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureColorStore.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 The Android Open Source Project + * Copyright (C) 2020 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. @@ -11,10 +11,10 @@ * 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 + * limitations under the License. */ -package com.android.systemui.statusbar.hvac; +package com.android.systemui.car.hvac; import android.graphics.Color; diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureTextAnimator.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextAnimator.java index 8ee5ef6badc3..74d970464108 100644 --- a/packages/CarSystemUI/src/com/android/systemui/statusbar/hvac/TemperatureTextAnimator.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextAnimator.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 The Android Open Source Project + * Copyright (C) 2020 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. @@ -11,13 +11,13 @@ * 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 + * limitations under the License. */ -package com.android.systemui.statusbar.hvac; +package com.android.systemui.car.hvac; -import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isHorizontal; -import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isLeft; +import static com.android.systemui.car.hvac.AnimatedTemperatureView.isHorizontal; +import static com.android.systemui.car.hvac.AnimatedTemperatureView.isLeft; import android.annotation.NonNull; import android.view.animation.AccelerateDecelerateInterpolator; diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureTextView.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextView.java index ad4fcd9b67da..521a665da5f6 100644 --- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureTextView.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureTextView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2020 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.navigationbar.car.hvac; +package com.android.systemui.car.hvac; import android.content.Context; import android.content.res.TypedArray; diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureView.java b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureView.java index 963f3184c40d..6b903fad505c 100644 --- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/hvac/TemperatureView.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/hvac/TemperatureView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2020 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.navigationbar.car.hvac; +package com.android.systemui.car.hvac; /** * Interface for Views that display temperature HVAC properties diff --git a/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java index 60ee19eec25f..4fde30987e50 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/keyguard/CarKeyguardViewController.java @@ -205,6 +205,9 @@ public class CarKeyguardViewController extends OverlayViewController implements @Override public void onCancelClicked() { + getOverlayViewGlobalStateController().setWindowFocusable(/* focusable= */ false); + getOverlayViewGlobalStateController().setWindowNeedsInput(/* needsInput= */ false); + mBouncer.hide(/* destroyView= */ true); mKeyguardCancelClickedListener.onCancelClicked(); } @@ -226,7 +229,8 @@ public class CarKeyguardViewController extends OverlayViewController implements @Override public void setNeedsInput(boolean needsInput) { - getLayout().setFocusable(needsInput); + getOverlayViewGlobalStateController().setWindowFocusable(needsInput); + getOverlayViewGlobalStateController().setWindowNeedsInput(needsInput); } /** diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java index 53e5d9fe91ec..20fcca0d0220 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java @@ -30,7 +30,6 @@ import com.android.car.notification.R; import com.android.car.notification.headsup.CarHeadsUpNotificationContainer; import com.android.systemui.car.CarDeviceProvisionedController; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.statusbar.car.CarStatusBar; import javax.inject.Inject; import javax.inject.Singleton; @@ -43,7 +42,7 @@ import dagger.Lazy; @Singleton public class CarHeadsUpNotificationSystemContainer implements CarHeadsUpNotificationContainer { private final CarDeviceProvisionedController mCarDeviceProvisionedController; - private final Lazy<CarStatusBar> mCarStatusBarLazy; + private final Lazy<NotificationPanelViewController> mNotificationPanelViewControllerLazy; private final ViewGroup mWindow; private final FrameLayout mHeadsUpContentFrame; @@ -55,10 +54,9 @@ public class CarHeadsUpNotificationSystemContainer implements CarHeadsUpNotifica @Main Resources resources, CarDeviceProvisionedController deviceProvisionedController, WindowManager windowManager, - // TODO: Remove dependency on CarStatusBar - Lazy<CarStatusBar> carStatusBarLazy) { + Lazy<NotificationPanelViewController> notificationPanelViewControllerLazy) { mCarDeviceProvisionedController = deviceProvisionedController; - mCarStatusBarLazy = carStatusBarLazy; + mNotificationPanelViewControllerLazy = notificationPanelViewControllerLazy; boolean showOnBottom = resources.getBoolean(R.bool.config_showHeadsUpNotificationOnBottom); @@ -87,7 +85,8 @@ public class CarHeadsUpNotificationSystemContainer implements CarHeadsUpNotifica private void animateShow() { if ((mEnableHeadsUpNotificationWhenNotificationShadeOpen - || !mCarStatusBarLazy.get().isPanelExpanded()) && isCurrentUserSetup()) { + || !mNotificationPanelViewControllerLazy.get().isPanelExpanded()) + && mCarDeviceProvisionedController.isCurrentUserFullySetup()) { mWindow.setVisibility(View.VISIBLE); } } @@ -114,9 +113,4 @@ public class CarHeadsUpNotificationSystemContainer implements CarHeadsUpNotifica public boolean isVisible() { return mWindow.getVisibility() == View.VISIBLE; } - - private boolean isCurrentUserSetup() { - return mCarDeviceProvisionedController.isCurrentUserSetup() - && !mCarDeviceProvisionedController.isCurrentUserSetupInProgress(); - } } diff --git a/packages/CarSystemUI/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetector.java b/packages/CarSystemUI/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetector.java index c0dbb5879d7d..f145b148eaf7 100644 --- a/packages/CarSystemUI/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetector.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetector.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.sideloaded.car; +package com.android.systemui.car.sideloaded; import android.annotation.NonNull; import android.app.ActivityManager; diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/CarStatusBarHeader.java index 4ef926fae816..bab67154e75d 100644 --- a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarStatusBarHeader.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/CarStatusBarHeader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018 The Android Open Source Project + * Copyright (C) 2020 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.qs.car; +package com.android.systemui.car.userswitcher; import android.content.Context; import android.graphics.Color; diff --git a/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java index 58add179886c..2ff667093e58 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/userswitcher/UserGridRecyclerView.java @@ -368,7 +368,7 @@ public class UserGridRecyclerView extends RecyclerView { private void applyCarSysUIDialogFlags(AlertDialog dialog) { final Window window = dialog.getWindow(); - window.setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); + window.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); window.getAttributes().setFitInsetsTypes( diff --git a/packages/CarSystemUI/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifier.java b/packages/CarSystemUI/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifier.java index 2f79f960f951..c054d204af98 100644 --- a/packages/CarSystemUI/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifier.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifier.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.voicerecognition.car; +package com.android.systemui.car.voicerecognition; import android.bluetooth.BluetoothHeadsetClient; import android.content.BroadcastReceiver; diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java index 9fdfc0fff307..2c2aec21ea4f 100644 --- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java +++ b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java @@ -52,7 +52,6 @@ import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.statusbar.AutoHideUiElement; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NavigationBarController; -import com.android.systemui.statusbar.SuperStatusBarViewFactory; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.BarTransitions; import com.android.systemui.statusbar.phone.PhoneStatusBarPolicy; @@ -80,7 +79,6 @@ public class CarNavigationBar extends SystemUI implements CommandQueue.Callbacks private final Handler mMainHandler; private final Lazy<KeyguardStateController> mKeyguardStateControllerLazy; private final Lazy<NavigationBarController> mNavigationBarControllerLazy; - private final SuperStatusBarViewFactory mSuperStatusBarViewFactory; private final ButtonSelectionStateController mButtonSelectionStateController; private final PhoneStatusBarPolicy mIconPolicy; private final StatusBarIconController mIconController; @@ -127,7 +125,6 @@ public class CarNavigationBar extends SystemUI implements CommandQueue.Callbacks @Main Handler mainHandler, Lazy<KeyguardStateController> keyguardStateControllerLazy, Lazy<NavigationBarController> navigationBarControllerLazy, - SuperStatusBarViewFactory superStatusBarViewFactory, ButtonSelectionStateController buttonSelectionStateController, PhoneStatusBarPolicy iconPolicy, StatusBarIconController iconController @@ -143,7 +140,6 @@ public class CarNavigationBar extends SystemUI implements CommandQueue.Callbacks mMainHandler = mainHandler; mKeyguardStateControllerLazy = keyguardStateControllerLazy; mNavigationBarControllerLazy = navigationBarControllerLazy; - mSuperStatusBarViewFactory = superStatusBarViewFactory; mButtonSelectionStateController = buttonSelectionStateController; mIconPolicy = iconPolicy; mIconController = iconController; diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java index 37a82255929a..8f3ae1a25441 100644 --- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java +++ b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBarController.java @@ -24,7 +24,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.R; -import com.android.systemui.navigationbar.car.hvac.HvacController; +import com.android.systemui.car.hvac.HvacController; import javax.inject.Inject; import javax.inject.Singleton; diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java deleted file mode 100644 index b74f1998bf9e..000000000000 --- a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFooter.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.car; - -import android.content.Context; -import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.util.Log; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import com.android.systemui.Dependency; -import com.android.systemui.R; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.qs.QSFooter; -import com.android.systemui.qs.QSPanel; -import com.android.systemui.statusbar.phone.MultiUserSwitch; -import com.android.systemui.statusbar.policy.DeviceProvisionedController; -import com.android.systemui.statusbar.policy.UserInfoController; - -/** - * The footer view that displays below the status bar in the auto use-case. This view shows the - * user switcher and access to settings. - */ -public class CarQSFooter extends RelativeLayout implements QSFooter, - UserInfoController.OnUserInfoChangedListener { - private static final String TAG = "CarQSFooter"; - - private UserInfoController mUserInfoController; - - private MultiUserSwitch mMultiUserSwitch; - private TextView mUserName; - private ImageView mMultiUserAvatar; - private CarQSFragment.UserSwitchCallback mUserSwitchCallback; - - public CarQSFooter(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mMultiUserSwitch = findViewById(R.id.multi_user_switch); - mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar); - mUserName = findViewById(R.id.user_name); - - mUserInfoController = Dependency.get(UserInfoController.class); - - mMultiUserSwitch.setOnClickListener(v -> { - if (mUserSwitchCallback == null) { - Log.e(TAG, "CarQSFooter not properly set up; cannot display user switcher."); - return; - } - - if (!mUserSwitchCallback.isShowing()) { - mUserSwitchCallback.show(); - } else { - mUserSwitchCallback.hide(); - } - }); - - findViewById(R.id.settings_button).setOnClickListener(v -> { - ActivityStarter activityStarter = Dependency.get(ActivityStarter.class); - - if (!Dependency.get(DeviceProvisionedController.class).isCurrentUserSetup()) { - // If user isn't setup just unlock the device and dump them back at SUW. - activityStarter.postQSRunnableDismissingKeyguard(() -> { }); - return; - } - - activityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), - true /* dismissShade */); - }); - } - - @Override - public void onUserInfoChanged(String name, Drawable picture, String userAccount) { - mMultiUserAvatar.setImageDrawable(picture); - mUserName.setText(name); - } - - @Override - public void setQSPanel(@Nullable QSPanel panel) { - if (panel != null) { - mMultiUserSwitch.setQsPanel(panel); - } - } - - public void setUserSwitchCallback(CarQSFragment.UserSwitchCallback callback) { - mUserSwitchCallback = callback; - } - - @Override - public void setListening(boolean listening) { - if (listening) { - mUserInfoController.addCallback(this); - } else { - mUserInfoController.removeCallback(this); - } - } - - @Override - public void setExpandClickListener(OnClickListener onClickListener) { - // No view that should expand/collapse the quick settings. - } - - @Override - public void setExpanded(boolean expanded) { - // Do nothing because the quick settings cannot be expanded. - } - - @Override - public void setExpansion(float expansion) { - // Do nothing because the quick settings cannot be expanded. - } - - @Override - public void setKeyguardShowing(boolean keyguardShowing) { - // Do nothing because the footer will not be shown when the keyguard is up. - } -} diff --git a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java b/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java deleted file mode 100644 index 31965c5fc022..000000000000 --- a/packages/CarSystemUI/src/com/android/systemui/qs/car/CarQSFragment.java +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.qs.car; - -import android.animation.Animator; -import android.animation.AnimatorInflater; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; -import android.app.Fragment; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; - -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.GridLayoutManager; - -import com.android.systemui.R; -import com.android.systemui.car.userswitcher.UserGridRecyclerView; -import com.android.systemui.plugins.qs.QS; -import com.android.systemui.qs.QSFooter; - -import java.util.ArrayList; -import java.util.List; - -/** - * A quick settings fragment for the car. For auto, there is no row for quick settings or ability - * to expand the quick settings panel. Instead, the only thing is that displayed is the - * status bar, and a static row with access to the user switcher and settings. - */ -public class CarQSFragment extends Fragment implements QS { - private View mHeader; - private View mUserSwitcherContainer; - private CarQSFooter mFooter; - private View mFooterUserName; - private View mFooterExpandIcon; - private UserGridRecyclerView mUserGridView; - private AnimatorSet mAnimatorSet; - private UserSwitchCallback mUserSwitchCallback; - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.car_qs_panel, container, false); - } - - @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - mHeader = view.findViewById(R.id.header); - mFooter = view.findViewById(R.id.qs_footer); - mFooterUserName = mFooter.findViewById(R.id.user_name); - mFooterExpandIcon = mFooter.findViewById(R.id.user_switch_expand_icon); - - mUserSwitcherContainer = view.findViewById(R.id.user_switcher_container); - - updateUserSwitcherHeight(0); - - Context context = getContext(); - mUserGridView = mUserSwitcherContainer.findViewById(R.id.user_grid); - GridLayoutManager layoutManager = new GridLayoutManager(context, - context.getResources().getInteger(R.integer.user_fullscreen_switcher_num_col)); - mUserGridView.setLayoutManager(layoutManager); - mUserGridView.buildAdapter(); - - mUserSwitchCallback = new UserSwitchCallback(); - mFooter.setUserSwitchCallback(mUserSwitchCallback); - } - - @Override - public void hideImmediately() { - getView().setVisibility(View.INVISIBLE); - } - - @Override - public void setQsExpansion(float qsExpansionFraction, float headerTranslation) { - // If the header is to be completed translated down, then set it to be visible. - getView().setVisibility(headerTranslation == 0 ? View.VISIBLE : View.INVISIBLE); - } - - @Override - public View getHeader() { - return mHeader; - } - - @VisibleForTesting - QSFooter getFooter() { - return mFooter; - } - - @Override - public void setHeaderListening(boolean listening) { - mFooter.setListening(listening); - } - - @Override - public void setListening(boolean listening) { - mFooter.setListening(listening); - } - - @Override - public int getQsMinExpansionHeight() { - return getView().getHeight(); - } - - @Override - public int getDesiredHeight() { - return getView().getHeight(); - } - - @Override - public void setPanelView(HeightListener notificationPanelView) { - // No quick settings panel. - } - - @Override - public void setHeightOverride(int desiredHeight) { - // No ability to expand quick settings. - } - - @Override - public void setHeaderClickable(boolean qsExpansionEnabled) { - // Usually this sets the expand button to be clickable, but there is no quick settings to - // expand. - } - - @Override - public boolean isCustomizing() { - // No ability to customize the quick settings. - return false; - } - - @Override - public void setOverscrolling(boolean overscrolling) { - // No overscrolling to reveal quick settings. - } - - @Override - public void setExpanded(boolean qsExpanded) { - // No quick settings to expand - } - - @Override - public boolean isShowingDetail() { - // No detail panel to close. - return false; - } - - @Override - public void closeDetail() { - // No detail panel to close. - } - - @Override - public void animateHeaderSlidingIn(long delay) { - // No header to animate. - } - - @Override - public void animateHeaderSlidingOut() { - // No header to animate. - } - - @Override - public void notifyCustomizeChanged() { - // There is no ability to customize quick settings. - } - - @Override - public void setContainer(ViewGroup container) { - // No quick settings, so no container to set. - } - - @Override - public void setExpandClickListener(OnClickListener onClickListener) { - // No ability to expand the quick settings. - } - - public class UserSwitchCallback { - private boolean mShowing; - - public boolean isShowing() { - return mShowing; - } - - public void show() { - mShowing = true; - animateHeightChange(true /* opening */); - } - - public void hide() { - mShowing = false; - animateHeightChange(false /* opening */); - } - } - - private void updateUserSwitcherHeight(int height) { - ViewGroup.LayoutParams layoutParams = mUserSwitcherContainer.getLayoutParams(); - layoutParams.height = height; - mUserSwitcherContainer.requestLayout(); - } - - private void animateHeightChange(boolean opening) { - // Animation in progress; cancel it to avoid contention. - if (mAnimatorSet != null) { - mAnimatorSet.cancel(); - } - - List<Animator> allAnimators = new ArrayList<>(); - ValueAnimator heightAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(getContext(), - opening ? R.anim.car_user_switcher_open_animation - : R.anim.car_user_switcher_close_animation); - heightAnimator.addUpdateListener(valueAnimator -> { - updateUserSwitcherHeight((Integer) valueAnimator.getAnimatedValue()); - }); - allAnimators.add(heightAnimator); - - Animator nameAnimator = AnimatorInflater.loadAnimator(getContext(), - opening ? R.anim.car_user_switcher_open_name_animation - : R.anim.car_user_switcher_close_name_animation); - nameAnimator.setTarget(mFooterUserName); - allAnimators.add(nameAnimator); - - Animator iconAnimator = AnimatorInflater.loadAnimator(getContext(), - opening ? R.anim.car_user_switcher_open_icon_animation - : R.anim.car_user_switcher_close_icon_animation); - iconAnimator.setTarget(mFooterExpandIcon); - allAnimators.add(iconAnimator); - - mAnimatorSet = new AnimatorSet(); - mAnimatorSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mAnimatorSet = null; - } - }); - mAnimatorSet.playTogether(allAnimators.toArray(new Animator[0])); - - // Setup all values to the start values in the animations, since there are delays, but need - // to have all values start at the beginning. - setupInitialValues(mAnimatorSet); - - mAnimatorSet.start(); - } - - private void setupInitialValues(Animator anim) { - if (anim instanceof AnimatorSet) { - for (Animator a : ((AnimatorSet) anim).getChildAnimations()) { - setupInitialValues(a); - } - } else if (anim instanceof ObjectAnimator) { - ((ObjectAnimator) anim).setCurrentFraction(0.0f); - } - } -} diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java index d8111d04348b..ec1dabc1bd72 100644 --- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java +++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java @@ -54,7 +54,6 @@ import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.PluginDependencyProvider; import com.android.systemui.plugins.qs.QS; -import com.android.systemui.qs.car.CarQSFragment; import com.android.systemui.recents.Recents; import com.android.systemui.recents.ScreenPinningRequest; import com.android.systemui.shared.plugins.PluginManager; @@ -407,7 +406,7 @@ public class CarStatusBar extends StatusBar implements CarBatteryController.Batt @Override protected QS createDefaultQSFragment() { - return new CarQSFragment(); + return null; } private BatteryController createBatteryController() { diff --git a/packages/CarSystemUI/src/com/android/systemui/window/OverlayViewGlobalStateController.java b/packages/CarSystemUI/src/com/android/systemui/window/OverlayViewGlobalStateController.java index 402d742cb949..5fe03f17919b 100644 --- a/packages/CarSystemUI/src/com/android/systemui/window/OverlayViewGlobalStateController.java +++ b/packages/CarSystemUI/src/com/android/systemui/window/OverlayViewGlobalStateController.java @@ -123,6 +123,12 @@ public class OverlayViewGlobalStateController { mSystemUIOverlayWindowController.setWindowFocusable(focusable); } + /** Sets the {@link android.view.WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM} flag of the + * sysui overlay window */ + public void setWindowNeedsInput(boolean needsInput) { + mSystemUIOverlayWindowController.setWindowNeedsInput(needsInput); + } + /** Returns {@code true} if the window is focusable. */ public boolean isWindowFocusable() { return mSystemUIOverlayWindowController.isWindowFocusable(); diff --git a/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java b/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java index 0dbe1a3ea1dd..5df5d6e98f18 100644 --- a/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java +++ b/packages/CarSystemUI/src/com/android/systemui/window/SystemUIOverlayWindowController.java @@ -90,7 +90,7 @@ public class SystemUIOverlayWindowController implements mLp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL, + WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH @@ -132,6 +132,16 @@ public class SystemUIOverlayWindowController implements updateWindow(); } + /** Sets the window to enable IME. */ + public void setWindowNeedsInput(boolean needsInput) { + if (needsInput) { + mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } else { + mLpChanged.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; + } + updateWindow(); + } + /** Returns {@code true} if the window is visible */ public boolean isWindowVisible() { return mVisible; diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/hvac/HvacControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/hvac/HvacControllerTest.java index a71d1db3ee70..7996170ba7d6 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/hvac/HvacControllerTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/hvac/HvacControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2020 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.navigationbar.car.hvac; +package com.android.systemui.car.hvac; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java index 05b8e6a54099..6ac72a681bfe 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java @@ -31,7 +31,6 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.car.CarDeviceProvisionedController; -import com.android.systemui.statusbar.car.CarStatusBar; import org.junit.Before; import org.junit.Test; @@ -48,7 +47,7 @@ public class CarHeadsUpNotificationSystemContainerTest extends SysuiTestCase { @Mock private CarDeviceProvisionedController mCarDeviceProvisionedController; @Mock - private CarStatusBar mCarStatusBar; + private NotificationPanelViewController mNotificationPanelViewController; @Mock private WindowManager mWindowManager; @@ -61,7 +60,7 @@ public class CarHeadsUpNotificationSystemContainerTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); - when(mCarStatusBar.isPanelExpanded()).thenReturn(false); + when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(false); when(mCarDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true); when(mCarDeviceProvisionedController.isCurrentUserSetupInProgress()).thenReturn(false); @@ -72,14 +71,14 @@ public class CarHeadsUpNotificationSystemContainerTest extends SysuiTestCase { mDefaultController = new CarHeadsUpNotificationSystemContainer(mContext, testableResources.getResources(), mCarDeviceProvisionedController, mWindowManager, - () -> mCarStatusBar); + () -> mNotificationPanelViewController); testableResources.addOverride( R.bool.config_enableHeadsUpNotificationWhenNotificationShadeOpen, true); mOverrideEnabledController = new CarHeadsUpNotificationSystemContainer(mContext, testableResources.getResources(), mCarDeviceProvisionedController, mWindowManager, - () -> mCarStatusBar); + () -> mNotificationPanelViewController); } @Test @@ -120,14 +119,14 @@ public class CarHeadsUpNotificationSystemContainerTest extends SysuiTestCase { @Test public void testDisplayNotification_notificationPanelExpanded_isInvisible() { - when(mCarStatusBar.isPanelExpanded()).thenReturn(true); + when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(true); mDefaultController.displayNotification(mNotificationView); assertThat(mDefaultController.isVisible()).isFalse(); } @Test public void testDisplayNotification_notificationPanelExpandedEnabledHUNWhenOpen_isVisible() { - when(mCarStatusBar.isPanelExpanded()).thenReturn(true); + when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(true); mOverrideEnabledController.displayNotification(mNotificationView); assertThat(mOverrideEnabledController.isVisible()).isTrue(); } diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetectorTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetectorTest.java index aebb0e005019..80f3d1ee5dec 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/sideloaded/car/CarSideLoadedAppDetectorTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/sideloaded/CarSideLoadedAppDetectorTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.sideloaded.car; +package com.android.systemui.car.sideloaded; import static com.google.common.truth.Truth.assertThat; diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifierTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifierTest.java index 38b47d0aea5d..eca51e34995c 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/voicerecognition/car/ConnectedDeviceVoiceRecognitionNotifierTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/voicerecognition/ConnectedDeviceVoiceRecognitionNotifierTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2020 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. @@ -14,10 +14,10 @@ * limitations under the License. */ -package com.android.systemui.voicerecognition.car; +package com.android.systemui.car.voicerecognition; -import static com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier.INVALID_VALUE; -import static com.android.systemui.voicerecognition.car.ConnectedDeviceVoiceRecognitionNotifier.VOICE_RECOGNITION_STARTED; +import static com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier.INVALID_VALUE; +import static com.android.systemui.car.voicerecognition.ConnectedDeviceVoiceRecognitionNotifier.VOICE_RECOGNITION_STARTED; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java index bbcd0d4eff81..28c69c776f17 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarControllerTest.java @@ -31,7 +31,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; -import com.android.systemui.navigationbar.car.hvac.HvacController; +import com.android.systemui.car.hvac.HvacController; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.statusbar.phone.StatusBarIconController; diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java index 3ecb29f95092..6da34d4dddc0 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/navigationbar/car/CarNavigationBarTest.java @@ -38,7 +38,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.car.CarDeviceProvisionedController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NavigationBarController; -import com.android.systemui.statusbar.SuperStatusBarViewFactory; import com.android.systemui.statusbar.phone.AutoHideController; import com.android.systemui.statusbar.phone.PhoneStatusBarPolicy; import com.android.systemui.statusbar.phone.StatusBarIconController; @@ -75,8 +74,6 @@ public class CarNavigationBarTest extends SysuiTestCase { @Mock private NavigationBarController mNavigationBarController; @Mock - private SuperStatusBarViewFactory mSuperStatusBarViewFactory; - @Mock private ButtonSelectionStateController mButtonSelectionStateController; @Mock private PhoneStatusBarPolicy mIconPolicy; @@ -92,8 +89,7 @@ public class CarNavigationBarTest extends SysuiTestCase { mCarNavigationBarController, mWindowManager, mDeviceProvisionedController, new CommandQueue(mContext), mAutoHideController, mButtonSelectionStateListener, mHandler, () -> mKeyguardStateController, () -> mNavigationBarController, - mSuperStatusBarViewFactory, mButtonSelectionStateController, mIconPolicy, - mIconController); + mButtonSelectionStateController, mIconPolicy, mIconController); } @Test diff --git a/packages/CtsShim/build/Android.bp b/packages/CtsShim/build/Android.bp index 587109d5dae9..be790106f42d 100644 --- a/packages/CtsShim/build/Android.bp +++ b/packages/CtsShim/build/Android.bp @@ -69,6 +69,15 @@ android_app { // Explicitly uncompress native libs rather than letting the build system doing it and destroy the // v2/v3 signature. use_embedded_native_libs: true, + apex_available: [ + "//apex_available:platform", + "com.android.apex.cts.shim.v1", + "com.android.apex.cts.shim.v2", + "com.android.apex.cts.shim.v2_no_hashtree", + "com.android.apex.cts.shim.v2_legacy", + "com.android.apex.cts.shim.v2_sdk_target_p", + "com.android.apex.cts.shim.v3", + ], } //########################################################## @@ -110,7 +119,11 @@ android_app { dex_preopt: { enabled: false, }, - manifest: "shim/AndroidManifestTargetPSdk.xml" + manifest: "shim/AndroidManifestTargetPSdk.xml", + apex_available: [ + "//apex_available:platform", + "com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p", + ], } //########################################################## @@ -128,4 +141,13 @@ android_app { }, manifest: "shim/AndroidManifest.xml", + apex_available: [ + "//apex_available:platform", + "com.android.apex.cts.shim.v1", + "com.android.apex.cts.shim.v2", + "com.android.apex.cts.shim.v2_no_hashtree", + "com.android.apex.cts.shim.v2_legacy", + "com.android.apex.cts.shim.v2_sdk_target_p", + "com.android.apex.cts.shim.v3", + ], } diff --git a/packages/CtsShim/build/jni/Android.bp b/packages/CtsShim/build/jni/Android.bp index ea15b43416b4..7a5b07e61e9d 100644 --- a/packages/CtsShim/build/jni/Android.bp +++ b/packages/CtsShim/build/jni/Android.bp @@ -18,4 +18,13 @@ cc_library_shared { name: "libshim_jni", srcs: ["Shim.c"], sdk_version: "24", + apex_available: [ + "//apex_available:platform", + "com.android.apex.cts.shim.v1", + "com.android.apex.cts.shim.v2", + "com.android.apex.cts.shim.v2_no_hashtree", + "com.android.apex.cts.shim.v2_legacy", + "com.android.apex.cts.shim.v2_sdk_target_p", + "com.android.apex.cts.shim.v3", + ], } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java index e551b69e024a..ee8fb38ef08c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java @@ -38,7 +38,7 @@ public class BluetoothMediaDevice extends MediaDevice { BluetoothMediaDevice(Context context, CachedBluetoothDevice device, MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) { - super(context, MediaDeviceType.TYPE_BLUETOOTH_DEVICE, routerManager, info, packageName); + super(context, routerManager, info, packageName); mCachedDevice = device; initDeviceRecord(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java index 85fa988a866e..83a96716e284 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java @@ -38,7 +38,7 @@ public class InfoMediaDevice extends MediaDevice { InfoMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) { - super(context, MediaDeviceType.TYPE_CAST_DEVICE, routerManager, info, packageName); + super(context, routerManager, info, packageName); initDeviceRecord(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index 39e6a129a992..6aff301f57d4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -15,6 +15,16 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_GROUP; +import static android.media.MediaRoute2Info.TYPE_HEARING_AID; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_TV; +import static android.media.MediaRoute2Info.TYPE_UNKNOWN; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; + import android.content.Context; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; @@ -38,13 +48,21 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { private static final String TAG = "MediaDevice"; @Retention(RetentionPolicy.SOURCE) - @IntDef({MediaDeviceType.TYPE_CAST_DEVICE, + @IntDef({MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE, + MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE, + MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE, MediaDeviceType.TYPE_BLUETOOTH_DEVICE, + MediaDeviceType.TYPE_CAST_DEVICE, + MediaDeviceType.TYPE_CAST_GROUP_DEVICE, MediaDeviceType.TYPE_PHONE_DEVICE}) public @interface MediaDeviceType { - int TYPE_PHONE_DEVICE = 1; - int TYPE_CAST_DEVICE = 2; - int TYPE_BLUETOOTH_DEVICE = 3; + int TYPE_USB_C_AUDIO_DEVICE = 1; + int TYPE_3POINT5_MM_AUDIO_DEVICE = 2; + int TYPE_FAST_PAIR_BLUETOOTH_DEVICE = 3; + int TYPE_BLUETOOTH_DEVICE = 4; + int TYPE_CAST_DEVICE = 5; + int TYPE_CAST_GROUP_DEVICE = 6; + int TYPE_PHONE_DEVICE = 7; } @VisibleForTesting @@ -58,13 +76,43 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { protected final MediaRouter2Manager mRouterManager; protected final String mPackageName; - MediaDevice(Context context, @MediaDeviceType int type, MediaRouter2Manager routerManager, - MediaRoute2Info info, String packageName) { - mType = type; + MediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, + String packageName) { mContext = context; mRouteInfo = info; mRouterManager = routerManager; mPackageName = packageName; + setType(info); + } + + private void setType(MediaRoute2Info info) { + if (info == null) { + mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; + return; + } + + switch (info.getType()) { + case TYPE_GROUP: + mType = MediaDeviceType.TYPE_CAST_GROUP_DEVICE; + break; + case TYPE_BUILTIN_SPEAKER: + mType = MediaDeviceType.TYPE_PHONE_DEVICE; + break; + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + mType = MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE; + break; + case TYPE_HEARING_AID: + case TYPE_BLUETOOTH_A2DP: + mType = MediaDeviceType.TYPE_BLUETOOTH_DEVICE; + break; + case TYPE_UNKNOWN: + case TYPE_REMOTE_TV: + case TYPE_REMOTE_SPEAKER: + default: + mType = MediaDeviceType.TYPE_CAST_DEVICE; + break; + } } void initDeviceRecord() { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index af88723c6249..c6c5ade90eb5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -42,7 +42,7 @@ public class PhoneMediaDevice extends MediaDevice { PhoneMediaDevice(Context context, MediaRouter2Manager routerManager, MediaRoute2Info info, String packageName) { - super(context, MediaDeviceType.TYPE_PHONE_DEVICE, routerManager, info, packageName); + super(context, routerManager, info, packageName); initDeviceRecord(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java index 653c8addba98..deccde593315 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/EnableZenModeDialog.java @@ -33,6 +33,7 @@ import android.util.Log; import android.util.Slog; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -395,8 +396,12 @@ public class EnableZenModeDialog { button1.setAlpha(button1.isEnabled() ? 1f : .5f); button2.setAlpha(button2.isEnabled() ? 1f : .5f); } else { - button1.setVisibility(View.GONE); - button2.setVisibility(View.GONE); + if (button1 != null) { + ((ViewGroup) row).removeView(button1); + } + if (button2 != null) { + ((ViewGroup) row).removeView(button2); + } } } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java index c713d7813a54..d7e76a14c768 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java @@ -133,6 +133,35 @@ public class WifiStatusTracker { } } + /** + * Fetches initial state as if a WifiManager.NETWORK_STATE_CHANGED_ACTION have been received. + * This replaces the dependency on the initial sticky broadcast. + */ + public void fetchInitialState() { + if (mWifiManager == null) { + return; + } + updateWifiState(); + final NetworkInfo networkInfo = + mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + connected = networkInfo != null && networkInfo.isConnected(); + mWifiInfo = null; + ssid = null; + if (connected) { + mWifiInfo = mWifiManager.getConnectionInfo(); + if (mWifiInfo != null) { + if (mWifiInfo.isPasspointAp() || mWifiInfo.isOsuAp()) { + ssid = mWifiInfo.getPasspointProviderFriendlyName(); + } else { + ssid = getValidSsid(mWifiInfo); + } + updateRssi(mWifiInfo.getRssi()); + maybeRequestNetworkScore(); + } + } + updateStatusLabel(); + } + public void handleBroadcast(Intent intent) { if (mWifiManager == null) { return; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java index 4b08387275be..db05b768f5db 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java @@ -15,6 +15,10 @@ */ package com.android.settingslib.media; +import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.verify; @@ -144,12 +148,19 @@ public class MediaDeviceTest { when(mCachedDevice2.isConnected()).thenReturn(true); when(mCachedDevice3.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(mCachedDevice3.isConnected()).thenReturn(true); + when(mBluetoothRouteInfo1.getType()).thenReturn(TYPE_BLUETOOTH_A2DP); + when(mBluetoothRouteInfo2.getType()).thenReturn(TYPE_BLUETOOTH_A2DP); + when(mBluetoothRouteInfo3.getType()).thenReturn(TYPE_BLUETOOTH_A2DP); when(mRouteInfo1.getId()).thenReturn(ROUTER_ID_1); when(mRouteInfo2.getId()).thenReturn(ROUTER_ID_2); when(mRouteInfo3.getId()).thenReturn(ROUTER_ID_3); when(mRouteInfo1.getName()).thenReturn(DEVICE_NAME_1); when(mRouteInfo2.getName()).thenReturn(DEVICE_NAME_2); when(mRouteInfo3.getName()).thenReturn(DEVICE_NAME_3); + when(mRouteInfo1.getType()).thenReturn(TYPE_REMOTE_SPEAKER); + when(mRouteInfo2.getType()).thenReturn(TYPE_REMOTE_SPEAKER); + when(mRouteInfo3.getType()).thenReturn(TYPE_REMOTE_SPEAKER); + when(mPhoneRouteInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager); when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); when(mProfileManager.getHearingAidProfile()).thenReturn(mHapProfile); @@ -271,12 +282,12 @@ public class MediaDeviceTest { @Test public void compareTo_info_bluetooth_infoFirst() { - mMediaDevices.add(mBluetoothMediaDevice1); mMediaDevices.add(mInfoMediaDevice1); + mMediaDevices.add(mBluetoothMediaDevice1); - assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); - Collections.sort(mMediaDevices, COMPARATOR); assertThat(mMediaDevices.get(0)).isEqualTo(mInfoMediaDevice1); + Collections.sort(mMediaDevices, COMPARATOR); + assertThat(mMediaDevices.get(0)).isEqualTo(mBluetoothMediaDevice1); } @Test @@ -327,7 +338,7 @@ public class MediaDeviceTest { // 5.mBluetoothMediaDevice2: * 2 times usage // 6.mBluetoothMediaDevice3: * 1 time usage // 7.mPhoneMediaDevice: * 0 time usage - // Order: 7 -> 2 -> 1 -> 3 -> 5 -> 4 -> 6 + // Order: 7 -> 2 -> 1 -> 5 -> 3 -> 6 -> 4 @Test public void compareTo_mixedDevices_carKitFirst() { when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass); @@ -352,10 +363,10 @@ public class MediaDeviceTest { assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice); assertThat(mMediaDevices.get(1)).isEqualTo(mBluetoothMediaDevice1); assertThat(mMediaDevices.get(2)).isEqualTo(mInfoMediaDevice1); - assertThat(mMediaDevices.get(3)).isEqualTo(mInfoMediaDevice2); - assertThat(mMediaDevices.get(4)).isEqualTo(mBluetoothMediaDevice2); - assertThat(mMediaDevices.get(5)).isEqualTo(mInfoMediaDevice3); - assertThat(mMediaDevices.get(6)).isEqualTo(mBluetoothMediaDevice3); + assertThat(mMediaDevices.get(3)).isEqualTo(mBluetoothMediaDevice2); + assertThat(mMediaDevices.get(4)).isEqualTo(mInfoMediaDevice2); + assertThat(mMediaDevices.get(5)).isEqualTo(mBluetoothMediaDevice3); + assertThat(mMediaDevices.get(6)).isEqualTo(mInfoMediaDevice3); } // 1.mInfoMediaDevice1: Last Selected device @@ -365,7 +376,7 @@ public class MediaDeviceTest { // 5.mBluetoothMediaDevice2: * 4 times usage not connected // 6.mBluetoothMediaDevice3: * 1 time usage // 7.mPhoneMediaDevice: * 0 time usage - // Order: 7 -> 1 -> 3 -> 4 -> 6 -> 2 -> 5 + // Order: 7 -> 1 -> 3 -> 6 -> 4 -> 2 -> 5 @Test public void compareTo_mixedDevices_connectDeviceFirst() { when(mDevice1.getBluetoothClass()).thenReturn(mCarkitClass); @@ -394,8 +405,8 @@ public class MediaDeviceTest { assertThat(mMediaDevices.get(0)).isEqualTo(mPhoneMediaDevice); assertThat(mMediaDevices.get(1)).isEqualTo(mInfoMediaDevice1); assertThat(mMediaDevices.get(2)).isEqualTo(mInfoMediaDevice2); - assertThat(mMediaDevices.get(3)).isEqualTo(mInfoMediaDevice3); - assertThat(mMediaDevices.get(4)).isEqualTo(mBluetoothMediaDevice3); + assertThat(mMediaDevices.get(3)).isEqualTo(mBluetoothMediaDevice3); + assertThat(mMediaDevices.get(4)).isEqualTo(mInfoMediaDevice3); assertThat(mMediaDevices.get(5)).isEqualTo(mBluetoothMediaDevice1); assertThat(mMediaDevices.get(6)).isEqualTo(mBluetoothMediaDevice2); } diff --git a/packages/SystemUI/res/drawable/bubble_manage_menu_row.xml b/packages/SystemUI/res/drawable/bubble_manage_menu_row.xml new file mode 100644 index 000000000000..a793680a037d --- /dev/null +++ b/packages/SystemUI/res/drawable/bubble_manage_menu_row.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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 + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true"> + <ripple android:color="#99999999" /> + </item> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/bubble_manage_menu.xml b/packages/SystemUI/res/layout/bubble_manage_menu.xml new file mode 100644 index 000000000000..129282dae77f --- /dev/null +++ b/packages/SystemUI/res/layout/bubble_manage_menu.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/rounded_bg_full" + android:elevation="@dimen/bubble_manage_menu_elevation" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/bubble_manage_menu_dismiss_container" + android:background="@drawable/bubble_manage_menu_row" + android:layout_width="match_parent" + android:layout_height="48dp" + android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:orientation="horizontal"> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_remove_no_shadow" + android:tint="@color/global_actions_text"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault" + android:text="@string/bubble_dismiss_text" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/bubble_manage_menu_dont_bubble_container" + android:background="@drawable/bubble_manage_menu_row" + android:layout_width="match_parent" + android:layout_height="48dp" + android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:orientation="horizontal"> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_stop_bubble" + android:tint="@color/global_actions_text"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault" + android:text="@string/bubbles_dont_bubble_conversation" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/bubble_manage_menu_settings_container" + android:background="@drawable/bubble_manage_menu_row" + android:layout_width="match_parent" + android:layout_height="48dp" + android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/bubble_manage_menu_settings_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:src="@drawable/ic_remove_no_shadow"/> + + <TextView + android:id="@+id/bubble_manage_menu_settings_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault" /> + + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/controls_management.xml b/packages/SystemUI/res/layout/controls_management.xml index ae57563cfb09..6da96d10c253 100644 --- a/packages/SystemUI/res/layout/controls_management.xml +++ b/packages/SystemUI/res/layout/controls_management.xml @@ -17,6 +17,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/controls_management_root" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 599ed1696ec9..179f8b8ea9f4 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1014,6 +1014,9 @@ <!-- Margins at the left and right of the power menu and home controls widgets. --> <dimen name="global_actions_side_margin">16dp</dimen> + <!-- Amount to shift the layout when exiting/entering for controls activities --> + <dimen name="global_actions_controls_y_translation">20dp</dimen> + <!-- The maximum offset in either direction that elements are moved horizontally to prevent burn-in on AOD. --> <dimen name="burn_in_prevention_offset_x">8dp</dimen> @@ -1197,6 +1200,7 @@ snap to the dismiss target. --> <dimen name="bubble_dismiss_target_padding_x">40dp</dimen> <dimen name="bubble_dismiss_target_padding_y">20dp</dimen> + <dimen name="bubble_manage_menu_elevation">4dp</dimen> <dimen name="dismiss_circle_size">52dp</dimen> <dimen name="dismiss_target_x_size">24dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index cb20e7a15424..d639ed074240 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2629,6 +2629,8 @@ <string name="bubbles_user_education_manage">Tap Manage to turn off bubbles from this app</string> <!-- Button text for dismissing the bubble "manage" button tool tip [CHAR LIMIT=20]--> <string name="bubbles_user_education_got_it">Got it</string> + <!-- Label for the button that takes the user to the notification settings for the given app. --> + <string name="bubbles_app_settings"><xliff:g id="notification_title" example="Android Messages">%1$s</xliff:g> settings</string> <!-- Notification content text when the system navigation mode changes as a result of changing the default launcher [CHAR LIMIT=NONE] --> <string name="notification_content_system_nav_changed">System navigation updated. To make changes, go to Settings.</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 7e24f5dbbd50..4ed819e4925b 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -663,8 +663,13 @@ <!-- Controls styles --> <style name="Theme.ControlsManagement" parent="@android:style/Theme.DeviceDefault.NoActionBar"> + <item name="android:windowActivityTransitions">true</item> + <item name="android:windowContentTransitions">false</item> <item name="android:windowIsTranslucent">false</item> - <item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item> + <item name="android:windowBackground">@android:color/black</item> + <item name="android:colorBackground">@android:color/black</item> + <item name="android:windowAnimationStyle">@null</item> + <item name="android:statusBarColor">@*android:color/transparent</item> </style> <style name="TextAppearance.Control"> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java index aa2fe3c7f8fc..57b3761c294f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardHostView.java @@ -16,7 +16,6 @@ package com.android.keyguard; -import android.app.Activity; import android.app.ActivityManager; import android.content.Context; import android.content.res.ColorStateList; @@ -31,6 +30,8 @@ import android.util.Log; import android.view.KeyEvent; import android.widget.FrameLayout; +import androidx.annotation.VisibleForTesting; + import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; @@ -101,7 +102,8 @@ public class KeyguardHostView extends FrameLayout implements SecurityCallback { public static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardViewBase"; - private KeyguardSecurityContainer mSecurityContainer; + @VisibleForTesting + protected KeyguardSecurityContainer mSecurityContainer; public KeyguardHostView(Context context) { this(context, null); @@ -446,4 +448,11 @@ public class KeyguardHostView extends FrameLayout implements SecurityCallback { public SecurityMode getCurrentSecurityMode() { return mSecurityContainer.getCurrentSecurityMode(); } + + /** + * When bouncer was visible and is starting to become hidden. + */ + public void onStartingToHide() { + mSecurityContainer.onStartingToHide(); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java index 718bcf16c832..65bf7e6e5025 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java @@ -152,6 +152,11 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView mImm.hideSoftInputFromWindow(getWindowToken(), 0); } + @Override + public void onStartingToHide() { + mImm.hideSoftInputFromWindow(getWindowToken(), 0); + } + private void updateSwitchImeButton() { // If there's more than one IME, enable the IME switcher button final boolean wasVisible = mSwitchImeButton.getVisibility() == View.VISIBLE; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 502c0787fe38..9cfcc52134ce 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -227,6 +227,13 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe } @Override + public void onStartingToHide() { + if (mCurrentSecuritySelection != SecurityMode.None) { + getSecurityView(mCurrentSecuritySelection).onStartingToHide(); + } + } + + @Override public boolean shouldDelayChildPressedState() { return true; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java index 20b1e0d2c822..43cef3acf147 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java @@ -159,4 +159,9 @@ public interface KeyguardSecurityView { default boolean disallowInterceptTouch(MotionEvent event) { return false; } + + /** + * When bouncer was visible but is being dragged down or dismissed. + */ + default void onStartingToHide() {}; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 367058fa58dd..a96ef91850df 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -1301,6 +1301,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private FingerprintManager mFpm; private FaceManager mFaceManager; private boolean mFingerprintLockedOut; + private TelephonyManager mTelephonyManager; /** * When we receive a @@ -1728,10 +1729,22 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } updateAirplaneModeState(); - TelephonyManager telephony = + mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - if (telephony != null) { - telephony.listen(mPhoneStateListener, LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE); + if (mTelephonyManager != null) { + mTelephonyManager.listen(mPhoneStateListener, + LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE); + // Set initial sim states values. + for (int slot = 0; slot < mTelephonyManager.getActiveModemCount(); slot++) { + int state = mTelephonyManager.getSimState(slot); + int[] subIds = mSubscriptionManager.getSubscriptionIds(slot); + if (subIds != null) { + for (int subId : subIds) { + mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, subId, slot, state) + .sendToTarget(); + } + } + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java index 409ae3f3c7d6..c92174a0d8af 100644 --- a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java +++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java @@ -164,7 +164,9 @@ public class PasswordTextView extends View { currentDrawPosition = getPaddingLeft(); } } else { - currentDrawPosition = getWidth() / 2 - totalDrawingWidth / 2; + float maxRight = getWidth() - getPaddingRight() - totalDrawingWidth; + float center = getWidth() / 2f - totalDrawingWidth / 2f; + currentDrawPosition = center > 0 ? center : maxRight; } int length = mTextChars.size(); Rect bounds = getCharBounds(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index 7262f8caac89..1f27ae238533 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -16,6 +16,8 @@ package com.android.systemui.accessibility; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; + import android.accessibilityservice.AccessibilityService; import android.app.PendingIntent; import android.app.RemoteAction; @@ -282,8 +284,8 @@ public class SystemActions extends SystemUI { private void handleTakeScreenshot() { ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext); - screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, - true, true, new Handler(Looper.getMainLooper()), null); + screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, true, true, + SCREENSHOT_GLOBAL_ACTIONS, new Handler(Looper.getMainLooper()), null); } private void handleAccessibilityMenu() { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index 71f2bc09b983..38bfffbb75d9 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -90,6 +90,7 @@ class Bubble implements BubbleViewProvider { } private FlyoutMessage mFlyoutMessage; + private Drawable mBadgedAppIcon; private Bitmap mBadgedImage; private int mDotColor; private Path mDotPath; @@ -133,6 +134,10 @@ class Bubble implements BubbleViewProvider { return mBadgedImage; } + public Drawable getBadgedAppIcon() { + return mBadgedAppIcon; + } + @Override public int getDotColor() { return mDotColor; @@ -239,6 +244,7 @@ class Bubble implements BubbleViewProvider { mAppName = info.appName; mFlyoutMessage = info.flyoutMessage; + mBadgedAppIcon = info.badgedAppIcon; mBadgedImage = info.badgedBubbleImage; mDotColor = info.dotColor; mDotPath = info.dotPath; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index c6883c89bd21..e488cf271fdf 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -605,6 +605,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); } + + mStackView.setUnbubbleConversationCallback(notificationEntry -> + onUserChangedBubble(notificationEntry, false /* shouldBubble */)); } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index 3524696dbc79..bb2365559f74 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -43,7 +43,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.ShapeDrawable; import android.os.RemoteException; -import android.service.notification.StatusBarNotification; import android.util.AttributeSet; import android.util.Log; import android.view.View; @@ -55,14 +54,13 @@ import com.android.internal.policy.ScreenDecorationsUtils; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.recents.TriangleShape; -import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.AlphaOptimizedButton; import com.android.systemui.statusbar.notification.collection.NotificationEntry; /** * Container for the expanded bubble view, handles rendering the caret and settings icon. */ -public class BubbleExpandedView extends LinearLayout implements View.OnClickListener { +public class BubbleExpandedView extends LinearLayout { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleExpandedView" : TAG_BUBBLES; private enum ActivityViewStatus { @@ -100,9 +98,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList private int mPointerWidth; private int mPointerHeight; private ShapeDrawable mPointerDrawable; - private Rect mTempRect = new Rect(); - private int[] mTempLoc = new int[2]; - private int mExpandedViewTouchSlop; @Nullable private Bubble mBubble; @@ -224,7 +219,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList mMinHeight = res.getDimensionPixelSize(R.dimen.bubble_expanded_default_height); mOverflowHeight = res.getDimensionPixelSize(R.dimen.bubble_overflow_height); mPointerMargin = res.getDimensionPixelSize(R.dimen.bubble_pointer_margin); - mExpandedViewTouchSlop = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_slop); } @Override @@ -239,7 +233,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList mPointerWidth = res.getDimensionPixelSize(R.dimen.bubble_pointer_width); mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height); - mPointerDrawable = new ShapeDrawable(TriangleShape.create( mPointerWidth, mPointerHeight, true /* pointUp */)); mPointerView.setBackground(mPointerDrawable); @@ -248,7 +241,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList mSettingsIconHeight = getContext().getResources().getDimensionPixelSize( R.dimen.bubble_manage_button_height); mSettingsIcon = findViewById(R.id.settings_button); - mSettingsIcon.setOnClickListener(this); mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */, true /* singleTaskInstance */); @@ -289,6 +281,19 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList return mBubble != null ? mBubble.getEntry() : null; } + void setManageClickListener(OnClickListener manageClickListener) { + findViewById(R.id.settings_button).setOnClickListener(manageClickListener); + } + + /** + * Updates the ActivityView's obscured touchable region. This calls onLocationChanged, which + * results in a call to {@link BubbleStackView#subtractObscuredTouchableRegion}. This is useful + * if a view has been added or removed from on top of the ActivityView, such as the manage menu. + */ + void updateObscuredTouchableRegion() { + mActivityView.onLocationChanged(); + } + void applyThemeAttrs() { final TypedArray ta = mContext.obtainStyledAttributes( new int[] { @@ -473,51 +478,6 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList } /** - * Whether the provided x, y values (in raw coordinates) are in a touchable area of the - * expanded view. - * - * The touchable areas are the ActivityView (plus some slop around it) and the manage button. - */ - boolean intersectingTouchableContent(int rawX, int rawY) { - mTempRect.setEmpty(); - if (mActivityView != null) { - mTempLoc = mActivityView.getLocationOnScreen(); - mTempRect.set(mTempLoc[0] - mExpandedViewTouchSlop, - mTempLoc[1] - mExpandedViewTouchSlop, - mTempLoc[0] + mActivityView.getWidth() + mExpandedViewTouchSlop, - mTempLoc[1] + mActivityView.getHeight() + mExpandedViewTouchSlop); - } - if (mTempRect.contains(rawX, rawY)) { - return true; - } - mTempLoc = mSettingsIcon.getLocationOnScreen(); - mTempRect.set(mTempLoc[0], - mTempLoc[1], - mTempLoc[0] + mSettingsIcon.getWidth(), - mTempLoc[1] + mSettingsIcon.getHeight()); - if (mTempRect.contains(rawX, rawY)) { - return true; - } - return false; - } - - @Override - public void onClick(View view) { - if (mBubble == null) { - return; - } - int id = view.getId(); - if (id == R.id.settings_button) { - Intent intent = mBubble.getSettingsIntent(); - mStackView.collapseStack(() -> { - mContext.startActivityAsUser(intent, mBubble.getEntry().getSbn().getUser()); - logBubbleClickEvent(mBubble, - SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); - }); - } - } - - /** * Update appearance of the expanded view being displayed. */ public void updateView() { @@ -547,10 +507,8 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList * Position of the manage button displayed in the expanded view. Used for placing user * education about the manage button. */ - public Rect getManageButtonLocationOnScreen() { - mTempLoc = mSettingsIcon.getLocationOnScreen(); - return new Rect(mTempLoc[0], mTempLoc[1], mTempLoc[0] + mSettingsIcon.getWidth(), - mTempLoc[1] + mSettingsIcon.getHeight()); + public void getManageButtonBoundsOnScreen(Rect rect) { + mSettingsIcon.getBoundsOnScreen(rect); } /** @@ -611,26 +569,4 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList } return INVALID_DISPLAY; } - - /** - * Logs bubble UI click event. - * - * @param bubble the bubble notification entry that user is interacting with. - * @param action the user interaction enum. - */ - private void logBubbleClickEvent(Bubble bubble, int action) { - StatusBarNotification notification = bubble.getEntry().getSbn(); - SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED, - notification.getPackageName(), - notification.getNotification().getChannelId(), - notification.getId(), - mStackView.getBubbleIndex(mStackView.getExpandedBubble()), - mStackView.getBubbleCount(), - action, - mStackView.getNormalizedXPosition(), - mStackView.getNormalizedYPosition(), - bubble.showInShade(), - bubble.isOngoing(), - false /* isAppForeground (unused) */); - } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 0aabdff96e1e..c9069316028c 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -33,12 +33,14 @@ import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.Notification; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; +import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Point; import android.graphics.PointF; @@ -47,6 +49,7 @@ import android.graphics.RectF; import android.graphics.Region; import android.os.Bundle; import android.os.Vibrator; +import android.service.notification.StatusBarNotification; import android.util.Log; import android.view.Choreographer; import android.view.DisplayCutout; @@ -55,6 +58,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.WindowManager; @@ -62,6 +66,7 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.MainThread; @@ -83,6 +88,7 @@ import com.android.systemui.bubbles.animation.StackAnimationController; import com.android.systemui.model.SysUiState; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.SysUiStatsLog; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.phone.NotificationShadeWindowController; import com.android.systemui.util.DismissCircleView; import com.android.systemui.util.FloatingContentCoordinator; @@ -97,6 +103,7 @@ import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Consumer; /** * Renders bubbles in a stack and handles animating expanded and collapsed states. @@ -224,7 +231,7 @@ public class BubbleStackView extends FrameLayout { private int mPointerHeight; private int mStatusBarHeight; private int mImeOffset; - private BubbleViewProvider mExpandedBubble; + @Nullable private BubbleViewProvider mExpandedBubble; private boolean mIsExpanded; /** Whether the stack is currently on the left side of the screen, or animating there. */ @@ -244,6 +251,10 @@ public class BubbleStackView extends FrameLayout { } private BubbleController.BubbleExpandListener mExpandListener; + + /** Callback to run when we want to unbubble the given notification's conversation. */ + private Consumer<NotificationEntry> mUnbubbleConversationCallback; + private SysUiState mSysUiState; private boolean mViewUpdatedRequested = false; @@ -255,9 +266,7 @@ public class BubbleStackView extends FrameLayout { private LayoutInflater mInflater; - // Used for determining view / touch intersection - int[] mTempLoc = new int[2]; - RectF mTempRect = new RectF(); + private Rect mTempRect = new Rect(); private final List<Rect> mSystemGestureExclusionRects = Collections.singletonList(new Rect()); @@ -471,6 +480,11 @@ public class BubbleStackView extends FrameLayout { return true; } + // If the manage menu is visible, just hide it. + if (mShowingManage) { + showManageMenu(false /* show */); + } + if (mBubbleData.isExpanded()) { maybeShowManageEducation(false /* show */); @@ -627,6 +641,13 @@ public class BubbleStackView extends FrameLayout { private BubbleManageEducationView mManageEducationView; private boolean mAnimatingManageEducationAway; + private ViewGroup mManageMenu; + private ImageView mManageSettingsIcon; + private TextView mManageSettingsText; + private boolean mShowingManage = false; + private PhysicsAnimator.SpringConfig mManageSpringConfig = new PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY); + @SuppressLint("ClickableViewAccessibility") public BubbleStackView(Context context, BubbleData data, @Nullable SurfaceSynchronizer synchronizer, @@ -689,6 +710,8 @@ public class BubbleStackView extends FrameLayout { mExpandedViewContainer.setClipChildren(false); addView(mExpandedViewContainer); + setUpManageMenu(); + setUpFlyout(); mFlyoutTransitionSpring.setSpring(new SpringForce() .setStiffness(SpringForce.STIFFNESS_LOW) @@ -838,7 +861,9 @@ public class BubbleStackView extends FrameLayout { // ActivityViews, etc.) were touched. Collapse the stack if it's expanded. setOnTouchListener((view, ev) -> { if (ev.getAction() == MotionEvent.ACTION_DOWN) { - if (mBubbleData.isExpanded()) { + if (mShowingManage) { + showManageMenu(false /* show */); + } else if (mBubbleData.isExpanded()) { mBubbleData.setExpanded(false); } } @@ -847,6 +872,66 @@ public class BubbleStackView extends FrameLayout { }); } + private void setUpManageMenu() { + if (mManageMenu != null) { + removeView(mManageMenu); + } + + mManageMenu = (ViewGroup) LayoutInflater.from(getContext()).inflate( + R.layout.bubble_manage_menu, this, false); + mManageMenu.setVisibility(View.INVISIBLE); + + PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig); + + final TypedArray ta = mContext.obtainStyledAttributes( + new int[] {android.R.attr.dialogCornerRadius}); + final int menuCornerRadius = ta.getDimensionPixelSize(0, 0); + ta.recycle(); + + mManageMenu.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), menuCornerRadius); + } + }); + mManageMenu.setClipToOutline(true); + + mManageMenu.findViewById(R.id.bubble_manage_menu_dismiss_container).setOnClickListener( + view -> { + showManageMenu(false /* show */); + dismissBubbleIfExists(mBubbleData.getSelectedBubble()); + }); + + mManageMenu.findViewById(R.id.bubble_manage_menu_dont_bubble_container).setOnClickListener( + view -> { + showManageMenu(false /* show */); + final Bubble bubble = mBubbleData.getSelectedBubble(); + if (bubble != null && mBubbleData.hasBubbleWithKey(bubble.getKey())) { + mUnbubbleConversationCallback.accept(bubble.getEntry()); + } + }); + + mManageMenu.findViewById(R.id.bubble_manage_menu_settings_container).setOnClickListener( + view -> { + showManageMenu(false /* show */); + final Bubble bubble = mBubbleData.getSelectedBubble(); + if (bubble != null && mBubbleData.hasBubbleWithKey(bubble.getKey())) { + final Intent intent = bubble.getSettingsIntent(); + collapseStack(() -> { + mContext.startActivityAsUser( + intent, bubble.getEntry().getSbn().getUser()); + logBubbleClickEvent( + bubble, + SysUiStatsLog.BUBBLE_UICHANGED__ACTION__HEADER_GO_TO_SETTINGS); + }); + } + }); + + mManageSettingsIcon = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_icon); + mManageSettingsText = mManageMenu.findViewById(R.id.bubble_manage_menu_settings_name); + addView(mManageMenu); + } + private void setUpUserEducation() { if (mUserEducationView != null) { removeView(mUserEducationView); @@ -934,6 +1019,7 @@ public class BubbleStackView extends FrameLayout { setUpFlyout(); setUpOverflow(); setUpUserEducation(); + setUpManageMenu(); } /** Respond to the phone being rotated by repositioning the stack and hiding any flyouts. */ @@ -960,6 +1046,9 @@ public class BubbleStackView extends FrameLayout { Math.max(0f, Math.min(1f, mVerticalPosPercentBeforeRotation)); addOnLayoutChangeListener(mOrientationChangedListener); hideFlyoutImmediate(); + + mManageMenu.setVisibility(View.INVISIBLE); + mShowingManage = false; } @Override @@ -1100,6 +1189,12 @@ public class BubbleStackView extends FrameLayout { mExpandListener = listener; } + /** Sets the function to call to un-bubble the given conversation. */ + public void setUnbubbleConversationCallback( + Consumer<NotificationEntry> unbubbleConversationCallback) { + mUnbubbleConversationCallback = unbubbleConversationCallback; + } + /** * Whether the stack of bubbles is expanded or not. */ @@ -1361,15 +1456,14 @@ public class BubbleStackView extends FrameLayout { mManageEducationView.setAlpha(0); mManageEducationView.setVisibility(VISIBLE); mManageEducationView.post(() -> { - final Rect position = - mExpandedBubble.getExpandedView().getManageButtonLocationOnScreen(); + mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); final int viewHeight = mManageEducationView.getManageViewHeight(); final int inset = getResources().getDimensionPixelSize( R.dimen.bubbles_manage_education_top_inset); mManageEducationView.bringToFront(); - mManageEducationView.setManageViewPosition(position.left, - position.top - viewHeight + inset); - mManageEducationView.setPointerPosition(position.centerX() - position.left); + mManageEducationView.setManageViewPosition(mTempRect.left, + mTempRect.top - viewHeight + inset); + mManageEducationView.setPointerPosition(mTempRect.centerX() - mTempRect.left); mManageEducationView.animate() .setDuration(ANIMATE_STACK_USER_EDUCATION_DURATION) .setInterpolator(FAST_OUT_SLOW_IN).alpha(1); @@ -1443,6 +1537,9 @@ public class BubbleStackView extends FrameLayout { } private void animateCollapse() { + // Hide the menu if it's visible. + showManageMenu(false); + mIsExpanded = false; final BubbleViewProvider previouslySelected = mExpandedBubble; beforeExpandedViewAnimation(); @@ -1570,9 +1667,9 @@ public class BubbleStackView extends FrameLayout { */ @Override public void subtractObscuredTouchableRegion(Region touchableRegion, View view) { - // If the notification shade is expanded, we shouldn't let the ActivityView steal any touch - // events from any location. - if (mNotificationShadeWindowController.getPanelExpanded()) { + // If the notification shade is expanded, or the manage menu is open, we shouldn't let the + // ActivityView steal any touch events from any location. + if (mNotificationShadeWindowController.getPanelExpanded() || mShowingManage) { touchableRegion.setEmpty(); } } @@ -1658,17 +1755,20 @@ public class BubbleStackView extends FrameLayout { private void dismissMagnetizedObject() { if (mIsExpanded) { final View draggedOutBubbleView = (View) mMagnetizedObject.getUnderlyingObject(); - final Bubble draggedOutBubble = mBubbleData.getBubbleWithView(draggedOutBubbleView); + dismissBubbleIfExists(mBubbleData.getBubbleWithView(draggedOutBubbleView)); - if (mBubbleData.hasBubbleWithKey(draggedOutBubble.getKey())) { - mBubbleData.notificationEntryRemoved( - draggedOutBubble.getEntry(), BubbleController.DISMISS_USER_GESTURE); - } } else { mBubbleData.dismissAll(BubbleController.DISMISS_USER_GESTURE); } } + private void dismissBubbleIfExists(@Nullable Bubble bubble) { + if (bubble != null && mBubbleData.hasBubbleWithKey(bubble.getKey())) { + mBubbleData.notificationEntryRemoved( + bubble.getEntry(), BubbleController.DISMISS_USER_GESTURE); + } + } + /** Prepares and starts the desaturate/darken animation on the bubble stack. */ private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) { mDesaturateAndDarkenTargetView = targetView; @@ -1912,6 +2012,63 @@ public class BubbleStackView extends FrameLayout { invalidate(); } + private void showManageMenu(boolean show) { + mShowingManage = show; + + // This should not happen, since the manage menu is only visible when there's an expanded + // bubble. If we end up in this state, just hide the menu immediately. + if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { + mManageMenu.setVisibility(View.INVISIBLE); + return; + } + + // If available, update the manage menu's settings option with the expanded bubble's app + // name and icon. + if (show && mBubbleData.hasBubbleWithKey(mExpandedBubble.getKey())) { + final Bubble bubble = mBubbleData.getBubbleWithKey(mExpandedBubble.getKey()); + mManageSettingsIcon.setImageDrawable(bubble.getBadgedAppIcon()); + mManageSettingsText.setText(getResources().getString( + R.string.bubbles_app_settings, bubble.getAppName())); + } + + mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect); + + // When the menu is open, it should be at these coordinates. This will make the menu's + // bottom left corner match up with the button's bottom left corner. + final float targetX = mTempRect.left; + final float targetY = mTempRect.bottom - mManageMenu.getHeight(); + + if (show) { + mManageMenu.setScaleX(0.5f); + mManageMenu.setScaleY(0.5f); + mManageMenu.setTranslationX(targetX - mManageMenu.getWidth() / 4); + mManageMenu.setTranslationY(targetY + mManageMenu.getHeight() / 4); + mManageMenu.setAlpha(0f); + + PhysicsAnimator.getInstance(mManageMenu) + .spring(DynamicAnimation.ALPHA, 1f) + .spring(DynamicAnimation.SCALE_X, 1f) + .spring(DynamicAnimation.SCALE_Y, 1f) + .spring(DynamicAnimation.TRANSLATION_X, targetX) + .spring(DynamicAnimation.TRANSLATION_Y, targetY) + .start(); + + mManageMenu.setVisibility(View.VISIBLE); + } else { + PhysicsAnimator.getInstance(mManageMenu) + .spring(DynamicAnimation.ALPHA, 0f) + .spring(DynamicAnimation.SCALE_X, 0.5f) + .spring(DynamicAnimation.SCALE_Y, 0.5f) + .spring(DynamicAnimation.TRANSLATION_X, targetX - mManageMenu.getWidth() / 4) + .spring(DynamicAnimation.TRANSLATION_Y, targetY + mManageMenu.getHeight() / 4) + .withEndActions(() -> mManageMenu.setVisibility(View.INVISIBLE)) + .start(); + } + + // Update the AV's obscured touchable region for the new menu visibility state. + mExpandedBubble.getExpandedView().updateObscuredTouchableRegion(); + } + private void updateExpandedBubble() { if (DEBUG_BUBBLE_STACK_VIEW) { Log.d(TAG, "updateExpandedBubble()"); @@ -1921,6 +2078,7 @@ public class BubbleStackView extends FrameLayout { && mExpandedBubble.getExpandedView() != null) { BubbleExpandedView bev = mExpandedBubble.getExpandedView(); mExpandedViewContainer.addView(bev); + bev.setManageClickListener((view) -> showManageMenu(!mShowingManage)); bev.populateExpandedView(); mExpandedViewContainer.setVisibility(VISIBLE); mExpandedViewContainer.setAlpha(1.0f); @@ -2089,4 +2247,26 @@ public class BubbleStackView extends FrameLayout { } return bubbles; } + + /** + * Logs bubble UI click event. + * + * @param bubble the bubble notification entry that user is interacting with. + * @param action the user interaction enum. + */ + private void logBubbleClickEvent(Bubble bubble, int action) { + StatusBarNotification notification = bubble.getEntry().getSbn(); + SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED, + notification.getPackageName(), + notification.getNotification().getChannelId(), + notification.getId(), + getBubbleIndex(getExpandedBubble()), + getBubbleCount(), + action, + getNormalizedXPosition(), + getNormalizedYPosition(), + bubble.showInShade(), + bubble.isOngoing(), + false /* isAppForeground (unused) */); + } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java index c96f9a470ca4..8a57a735f6cb 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewInfoTask.java @@ -116,6 +116,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask ShortcutInfo shortcutInfo; String appName; Bitmap badgedBubbleImage; + Drawable badgedAppIcon; int dotColor; Path dotPath; Bubble.FlyoutMessage flyoutMessage; @@ -176,6 +177,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask } BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon); + info.badgedAppIcon = badgedIcon; info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable, badgeBitmapInfo).icon; diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java index ef84c73b3145..ca3e2e27fa9a 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleViewProvider.java @@ -20,15 +20,17 @@ import android.graphics.Bitmap; import android.graphics.Path; import android.view.View; +import androidx.annotation.Nullable; + /** * Interface to represent actual Bubbles and UI elements that act like bubbles, like BubbleOverflow. */ interface BubbleViewProvider { - BubbleExpandedView getExpandedView(); + @Nullable BubbleExpandedView getExpandedView(); void setContentVisibility(boolean visible); - View getIconView(); + @Nullable View getIconView(); void logUIEvent(int bubbleCount, int action, float normalX, float normalY, int index); diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt index 7e8fec716b1f..e84f439c1fe2 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsBindingControllerImpl.kt @@ -31,6 +31,7 @@ import com.android.internal.annotations.VisibleForTesting import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Singleton @@ -284,13 +285,19 @@ open class ControlsBindingControllerImpl @Inject constructor( val requestLimit: Long ) : IControlsSubscriber.Stub() { val loadedControls = ArrayList<Control>() - private var isTerminated = false + private var isTerminated = AtomicBoolean(false) private var _loadCancelInternal: (() -> Unit)? = null private lateinit var subscription: IControlsSubscription + /** + * Potentially cancel a subscriber. The subscriber may also have terminated, in which case + * the request is ignored. + */ fun loadCancel() = Runnable { - Log.d(TAG, "Cancel load requested") - _loadCancelInternal?.invoke() + _loadCancelInternal?.let { + Log.d(TAG, "Canceling loadSubscribtion") + it.invoke() + } } override fun onSubscribe(token: IBinder, subs: IControlsSubscription) { @@ -301,7 +308,7 @@ open class ControlsBindingControllerImpl @Inject constructor( override fun onNext(token: IBinder, c: Control) { backgroundExecutor.execute { - if (isTerminated) return@execute + if (isTerminated.get()) return@execute loadedControls.add(c) @@ -325,13 +332,15 @@ open class ControlsBindingControllerImpl @Inject constructor( } private fun maybeTerminateAndRun(postTerminateFn: Runnable) { - if (isTerminated) return + if (isTerminated.get()) return - isTerminated = true _loadCancelInternal = {} currentProvider?.cancelLoadTimeout() - backgroundExecutor.execute(postTerminateFn) + backgroundExecutor.execute { + isTerminated.compareAndSet(false, true) + postTerminateFn.run() + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt index 79a5b0a1ce52..bc97c10756fd 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsController.kt @@ -52,19 +52,17 @@ interface ControlsController : UserAwareController { * Load all available [Control] for a given service. * * @param componentName the [ComponentName] of the [ControlsProviderService] to load from - * @param dataCallback a callback in which to retrieve the result. + * @param dataCallback a callback in which to retrieve the result + * @param cancelWrapper a callback to receive a [Runnable] that can be run to cancel the + * request */ fun loadForComponent( componentName: ComponentName, - dataCallback: Consumer<LoadData> + dataCallback: Consumer<LoadData>, + cancelWrapper: Consumer<Runnable> ) /** - * Cancels a pending load call - */ - fun cancelLoad() - - /** * Request to subscribe for favorited controls per structure * * @param structureInfo structure to limit the subscription to diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt index 5626a5de2e3c..8e88756b16fd 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt @@ -77,8 +77,6 @@ class ControlsControllerImpl @Inject constructor ( private var userChanging: Boolean = true - private var loadCanceller: Runnable? = null - private var seedingInProgress = false private val seedingCallbacks = mutableListOf<Consumer<Boolean>>() @@ -276,28 +274,29 @@ class ControlsControllerImpl @Inject constructor ( override fun loadForComponent( componentName: ComponentName, - dataCallback: Consumer<ControlsController.LoadData> + dataCallback: Consumer<ControlsController.LoadData>, + cancelWrapper: Consumer<Runnable> ) { if (!confirmAvailability()) { if (userChanging) { // Try again later, userChanging should not last forever. If so, we have bigger // problems. This will return a runnable that allows to cancel the delayed version, // it will not be able to cancel the load if - loadCanceller = executor.executeDelayed( - { loadForComponent(componentName, dataCallback) }, - USER_CHANGE_RETRY_DELAY, - TimeUnit.MILLISECONDS + executor.executeDelayed( + { loadForComponent(componentName, dataCallback, cancelWrapper) }, + USER_CHANGE_RETRY_DELAY, + TimeUnit.MILLISECONDS ) - } else { - dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true)) } - return + + dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true)) } - loadCanceller = bindingController.bindAndLoad( + + cancelWrapper.accept( + bindingController.bindAndLoad( componentName, object : ControlsBindingController.LoadCallback { override fun accept(controls: List<Control>) { - loadCanceller = null executor.execute { val favoritesForComponentKeys = Favorites .getControlsForComponent(componentName).map { it.controlId } @@ -333,7 +332,6 @@ class ControlsControllerImpl @Inject constructor ( } override fun error(message: String) { - loadCanceller = null executor.execute { val controls = Favorites.getStructuresForComponent(componentName) .flatMap { st -> @@ -348,6 +346,7 @@ class ControlsControllerImpl @Inject constructor ( } } } + ) ) } @@ -432,12 +431,6 @@ class ControlsControllerImpl @Inject constructor ( seedingCallbacks.clear() } - override fun cancelLoad() { - loadCanceller?.let { - executor.execute(it) - } - } - private fun createRemovedStatus( componentName: ComponentName, controlInfo: ControlInfo, diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt new file mode 100644 index 000000000000..4ca47d1d41ba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.controls.management + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.annotation.IdRes +import android.content.Intent + +import android.transition.Transition +import android.transition.TransitionValues +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.Window + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent + +import com.android.systemui.Interpolators +import com.android.systemui.R + +import com.android.systemui.controls.ui.ControlsUiController + +object ControlsAnimations { + + private const val ALPHA_EXIT_DURATION = 167L + private const val ALPHA_ENTER_DELAY = ALPHA_EXIT_DURATION + private const val ALPHA_ENTER_DURATION = 350L - ALPHA_ENTER_DELAY + + private const val Y_TRANSLATION_EXIT_DURATION = 183L + private const val Y_TRANSLATION_ENTER_DELAY = Y_TRANSLATION_EXIT_DURATION - ALPHA_ENTER_DELAY + private const val Y_TRANSLATION_ENTER_DURATION = 400L - Y_TRANSLATION_EXIT_DURATION + private var translationY: Float = -1f + + /** + * Setup an activity to handle enter/exit animations. [view] should be the root of the content. + * Fade and translate together. + */ + fun observerForAnimations(view: ViewGroup, window: Window, intent: Intent): LifecycleObserver { + return object : LifecycleObserver { + var showAnimation = intent.getBooleanExtra(ControlsUiController.EXTRA_ANIMATE, false) + + init { + // Must flag the parent group to move it all together, and set the initial + // transitionAlpha to 0.0f. This property is reserved for fade animations. + view.setTransitionGroup(true) + view.transitionAlpha = 0.0f + + if (translationY == -1f) { + translationY = view.context.resources.getDimensionPixelSize( + R.dimen.global_actions_controls_y_translation).toFloat() + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + fun setup() { + with(window) { + allowEnterTransitionOverlap = true + enterTransition = enterWindowTransition(view.getId()) + exitTransition = exitWindowTransition(view.getId()) + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun enterAnimation() { + if (showAnimation) { + ControlsAnimations.enterAnimation(view).start() + showAnimation = false + } + } + } + } + + fun enterAnimation(view: View): Animator { + Log.d(ControlsUiController.TAG, "Enter animation for $view") + + view.transitionAlpha = 0.0f + view.alpha = 1.0f + + view.translationY = translationY + + val alphaAnimator = ObjectAnimator.ofFloat(view, "transitionAlpha", 0.0f, 1.0f).apply { + interpolator = Interpolators.DECELERATE_QUINT + startDelay = ALPHA_ENTER_DELAY + duration = ALPHA_ENTER_DURATION + } + + val yAnimator = ObjectAnimator.ofFloat(view, "translationY", 0.0f).apply { + interpolator = Interpolators.DECELERATE_QUINT + startDelay = Y_TRANSLATION_ENTER_DURATION + duration = Y_TRANSLATION_ENTER_DURATION + } + + return AnimatorSet().apply { + playTogether(alphaAnimator, yAnimator) + } + } + + /** + * Properly handle animations originating from dialogs. Activity transitions require + * transitioning between two activities, so expose this method for dialogs to animate + * on exit. + */ + @JvmStatic + fun exitAnimation(view: View, onEnd: Runnable? = null): Animator { + Log.d(ControlsUiController.TAG, "Exit animation for $view") + + val alphaAnimator = ObjectAnimator.ofFloat(view, "transitionAlpha", 0.0f).apply { + interpolator = Interpolators.ACCELERATE + duration = ALPHA_EXIT_DURATION + } + + view.translationY = 0.0f + val yAnimator = ObjectAnimator.ofFloat(view, "translationY", -translationY).apply { + interpolator = Interpolators.ACCELERATE + duration = Y_TRANSLATION_EXIT_DURATION + } + + return AnimatorSet().apply { + playTogether(alphaAnimator, yAnimator) + onEnd?.let { + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + it.run() + } + }) + } + } + } + + fun enterWindowTransition(@IdRes id: Int) = + WindowTransition({ view: View -> enterAnimation(view) }).apply { + addTarget(id) + } + + fun exitWindowTransition(@IdRes id: Int) = + WindowTransition({ view: View -> exitAnimation(view) }).apply { + addTarget(id) + } +} + +/** + * In order to animate, at least one property must be marked on each view that should move. + * Setting "item" is just a flag to indicate that it should move by the animator. + */ +class WindowTransition( + val animator: (view: View) -> Animator +) : Transition() { + override fun captureStartValues(tv: TransitionValues) { + tv.values["item"] = 0.0f + } + + override fun captureEndValues(tv: TransitionValues) { + tv.values["item"] = 1.0f + } + + override fun createAnimator( + sceneRoot: ViewGroup, + startValues: TransitionValues?, + endValues: TransitionValues? + ): Animator? = animator(startValues!!.view) +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt index ee1ce7ab3d83..640c90d2ba59 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt @@ -16,11 +16,12 @@ package com.android.systemui.controls.management -import android.app.Activity +import android.app.ActivityOptions import android.content.ComponentName import android.content.Intent import android.os.Bundle import android.view.View +import android.view.ViewGroup import android.view.ViewStub import android.widget.Button import android.widget.TextView @@ -31,7 +32,9 @@ import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.controller.ControlsControllerImpl import com.android.systemui.controls.controller.StructureInfo +import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.util.LifecycleActivity import javax.inject.Inject /** @@ -39,8 +42,9 @@ import javax.inject.Inject */ class ControlsEditingActivity @Inject constructor( private val controller: ControlsControllerImpl, - broadcastDispatcher: BroadcastDispatcher -) : Activity() { + broadcastDispatcher: BroadcastDispatcher, + private val globalActionsComponent: GlobalActionsComponent +) : LifecycleActivity() { companion object { private const val TAG = "ControlsEditingActivity" @@ -84,14 +88,31 @@ class ControlsEditingActivity @Inject constructor( bindViews() bindButtons() + } + override fun onStart() { + super.onStart() setUpList() currentUserTracker.startTracking() } + override fun onStop() { + super.onStop() + currentUserTracker.stopTracking() + } + private fun bindViews() { setContentView(R.layout.controls_management) + + getLifecycle().addObserver( + ControlsAnimations.observerForAnimations( + requireViewById<ViewGroup>(R.id.controls_management_root), + window, + intent + ) + ) + requireViewById<ViewStub>(R.id.stub).apply { layoutResource = R.layout.controls_management_editing inflate() @@ -113,17 +134,26 @@ class ControlsEditingActivity @Inject constructor( putExtras(this@ControlsEditingActivity.intent) putExtra(ControlsFavoritingActivity.EXTRA_SINGLE_STRUCTURE, true) } - startActivity(intent) - finish() + startActivity(intent, ActivityOptions + .makeSceneTransitionAnimation(this@ControlsEditingActivity).toBundle()) } } + val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) saveButton = requireViewById<Button>(R.id.done).apply { isEnabled = false setText(R.string.save) setOnClickListener { saveFavorites() - finishAffinity() + ControlsAnimations.exitAnimation( + rootView, + object : Runnable { + override fun run() { + finish() + } + } + ).start() + globalActionsComponent.handleShowGlobalActionsMenu() } } } @@ -151,26 +181,38 @@ class ControlsEditingActivity @Inject constructor( val controls = controller.getFavoritesForStructure(component, structure) model = FavoritesModel(component, controls, favoritesModelCallback) val elevation = resources.getFloat(R.dimen.control_card_elevation) - val adapter = ControlAdapter(elevation) - val recycler = requireViewById<RecyclerView>(R.id.list) + val recyclerView = requireViewById<RecyclerView>(R.id.list) + recyclerView.alpha = 0.0f + val adapter = ControlAdapter(elevation).apply { + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + var hasAnimated = false + override fun onChanged() { + if (!hasAnimated) { + hasAnimated = true + ControlsAnimations.enterAnimation(recyclerView).start() + } + } + }) + } + val margin = resources .getDimensionPixelSize(R.dimen.controls_card_margin) val itemDecorator = MarginItemDecorator(margin, margin) - recycler.apply { + recyclerView.apply { this.adapter = adapter - layoutManager = GridLayoutManager(recycler.context, 2).apply { + layoutManager = GridLayoutManager(recyclerView.context, 2).apply { spanSizeLookup = adapter.spanSizeLookup } addItemDecoration(itemDecorator) } adapter.changeModel(model) model.attachAdapter(adapter) - ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recycler) + ItemTouchHelper(model.itemTouchHelperCallback).attachToRecyclerView(recyclerView) } override fun onDestroy() { currentUserTracker.stopTracking() super.onDestroy() } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt index 6f34deeb8547..183bd7bb2b7a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsFavoritingActivity.kt @@ -16,7 +16,7 @@ package com.android.systemui.controls.management -import android.app.Activity +import android.app.ActivityOptions import android.content.ComponentName import android.content.Intent import android.content.res.Configuration @@ -40,7 +40,9 @@ import com.android.systemui.controls.TooltipManager import com.android.systemui.controls.controller.ControlsControllerImpl import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.settings.CurrentUserTracker +import com.android.systemui.util.LifecycleActivity import java.text.Collator import java.util.concurrent.Executor import java.util.function.Consumer @@ -50,8 +52,9 @@ class ControlsFavoritingActivity @Inject constructor( @Main private val executor: Executor, private val controller: ControlsControllerImpl, private val listingController: ControlsListingController, - broadcastDispatcher: BroadcastDispatcher -) : Activity() { + broadcastDispatcher: BroadcastDispatcher, + private val globalActionsComponent: GlobalActionsComponent +) : LifecycleActivity() { companion object { private const val TAG = "ControlsFavoritingActivity" @@ -81,6 +84,8 @@ class ControlsFavoritingActivity @Inject constructor( private var listOfStructures = emptyList<StructureContainer>() private lateinit var comparator: Comparator<StructureContainer> + private var cancelLoadRunnable: Runnable? = null + private var isPagerLoaded = false private val currentUserTracker = object : CurrentUserTracker(broadcastDispatcher) { private val startingUser = controller.currentUserId @@ -115,6 +120,7 @@ class ControlsFavoritingActivity @Inject constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val collator = Collator.getInstance(resources.configuration.locales[0]) comparator = compareBy(collator) { it.structureName } appName = intent.getCharSequenceExtra(EXTRA_APP) @@ -122,14 +128,6 @@ class ControlsFavoritingActivity @Inject constructor( component = intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME) bindViews() - - setUpPager() - - loadControls() - - listingController.addCallback(listingCallback) - - currentUserTracker.startTracking() } private val controlsModelCallback = object : ControlsModel.ControlsModelCallback { @@ -174,12 +172,17 @@ class ControlsFavoritingActivity @Inject constructor( pageIndicator.setLocation(0f) pageIndicator.visibility = if (listOfStructures.size > 1) View.VISIBLE else View.GONE + + ControlsAnimations.enterAnimation(pageIndicator).start() + ControlsAnimations.enterAnimation(structurePager).start() } - }) + }, Consumer { runnable -> cancelLoadRunnable = runnable }) } } private fun setUpPager() { + structurePager.alpha = 0.0f + pageIndicator.alpha = 0.0f structurePager.apply { adapter = StructureAdapter(emptyList()) registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { @@ -203,6 +206,15 @@ class ControlsFavoritingActivity @Inject constructor( private fun bindViews() { setContentView(R.layout.controls_management) + + getLifecycle().addObserver( + ControlsAnimations.observerForAnimations( + requireViewById<ViewGroup>(R.id.controls_management_root), + window, + intent + ) + ) + requireViewById<ViewStub>(R.id.stub).apply { layoutResource = R.layout.controls_management_favorites inflate() @@ -278,10 +290,12 @@ class ControlsFavoritingActivity @Inject constructor( val i = Intent() i.setComponent( ComponentName(context, ControlsProviderSelectorActivity::class.java)) - context.startActivity(i) + startActivity(i, ActivityOptions + .makeSceneTransitionAnimation(this@ControlsFavoritingActivity).toBundle()) } } + val rootView = requireViewById<ViewGroup>(R.id.controls_management_root) doneButton = requireViewById<Button>(R.id.done).apply { isEnabled = false setOnClickListener { @@ -292,7 +306,16 @@ class ControlsFavoritingActivity @Inject constructor( StructureInfo(component!!, it.structureName, favoritesForStorage) ) } - finishAffinity() + + ControlsAnimations.exitAnimation( + rootView, + object : Runnable { + override fun run() { + finish() + } + } + ).start() + globalActionsComponent.handleShowGlobalActionsMenu() } } } @@ -302,15 +325,39 @@ class ControlsFavoritingActivity @Inject constructor( mTooltipManager?.hide(false) } + override fun onStart() { + super.onStart() + + listingController.addCallback(listingCallback) + currentUserTracker.startTracking() + } + + override fun onResume() { + super.onResume() + + // only do once, to make sure that any user changes do not get replaces if resume is called + // more than once + if (!isPagerLoaded) { + setUpPager() + loadControls() + isPagerLoaded = true + } + } + + override fun onStop() { + super.onStop() + + listingController.removeCallback(listingCallback) + currentUserTracker.stopTracking() + } + override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) mTooltipManager?.hide(false) } override fun onDestroy() { - currentUserTracker.stopTracking() - listingController.removeCallback(listingCallback) - controller.cancelLoad() + cancelLoadRunnable?.run() super.onDestroy() } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt index 3be59009f531..80cb96803f24 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsProviderSelectorActivity.kt @@ -16,15 +16,18 @@ package com.android.systemui.controls.management +import android.app.ActivityOptions import android.content.ComponentName import android.content.Intent import android.os.Bundle import android.view.LayoutInflater +import android.view.ViewGroup import android.view.ViewStub import android.widget.Button import android.widget.TextView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.controls.controller.ControlsController @@ -66,12 +69,37 @@ class ControlsProviderSelectorActivity @Inject constructor( super.onCreate(savedInstanceState) setContentView(R.layout.controls_management) + + getLifecycle().addObserver( + ControlsAnimations.observerForAnimations( + requireViewById<ViewGroup>(R.id.controls_management_root), + window, + intent + ) + ) + requireViewById<ViewStub>(R.id.stub).apply { layoutResource = R.layout.controls_management_apps inflate() } recyclerView = requireViewById(R.id.list) + recyclerView.layoutManager = LinearLayoutManager(applicationContext) + + requireViewById<TextView>(R.id.title).apply { + text = resources.getText(R.string.controls_providers_title) + } + + requireViewById<Button>(R.id.done).setOnClickListener { + this@ControlsProviderSelectorActivity.finishAffinity() + } + } + + override fun onStart() { + super.onStart() + currentUserTracker.startTracking() + + recyclerView.alpha = 0.0f recyclerView.adapter = AppAdapter( backExecutor, executor, @@ -80,17 +108,22 @@ class ControlsProviderSelectorActivity @Inject constructor( LayoutInflater.from(this), ::launchFavoritingActivity, FavoritesRenderer(resources, controlsController::countFavoritesForComponent), - resources) - recyclerView.layoutManager = LinearLayoutManager(applicationContext) - - requireViewById<TextView>(R.id.title).text = - resources.getText(R.string.controls_providers_title) - - requireViewById<Button>(R.id.done).setOnClickListener { - this@ControlsProviderSelectorActivity.finishAffinity() + resources).apply { + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + var hasAnimated = false + override fun onChanged() { + if (!hasAnimated) { + hasAnimated = true + ControlsAnimations.enterAnimation(recyclerView).start() + } + } + }) } + } - currentUserTracker.startTracking() + override fun onStop() { + super.onStop() + currentUserTracker.stopTracking() } /** @@ -98,7 +131,7 @@ class ControlsProviderSelectorActivity @Inject constructor( * @param component a component name for a [ControlsProviderService] */ fun launchFavoritingActivity(component: ComponentName?) { - backExecutor.execute { + executor.execute { component?.let { val intent = Intent(applicationContext, ControlsFavoritingActivity::class.java) .apply { @@ -107,7 +140,7 @@ class ControlsProviderSelectorActivity @Inject constructor( putExtra(Intent.EXTRA_COMPONENT_NAME, it) flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } - startActivity(intent) + startActivity(intent, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt index 61a1a986c091..aed7cd316bc7 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiController.kt @@ -26,9 +26,10 @@ interface ControlsUiController { companion object { public const val TAG = "ControlsUiController" + public const val EXTRA_ANIMATE = "extra_animate" } - fun show(parent: ViewGroup) + fun show(parent: ViewGroup, dismissGlobalActions: Runnable) fun hide() fun onRefreshState(componentName: ComponentName, controls: List<Control>) fun onActionResponse( diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 2adfb1bd6d4f..cfd8df059567 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -85,7 +85,7 @@ class ControlsUiControllerImpl @Inject constructor ( private const val PREF_STRUCTURE = "controls_structure" private const val USE_PANELS = "systemui.controls_use_panel" - private const val FADE_IN_MILLIS = 225L + private const val FADE_IN_MILLIS = 200L private val EMPTY_COMPONENT = ComponentName("", "") private val EMPTY_STRUCTURE = StructureInfo( @@ -104,6 +104,7 @@ class ControlsUiControllerImpl @Inject constructor ( private var popup: ListPopupWindow? = null private var activeDialog: Dialog? = null private var hidden = true + private lateinit var dismissGlobalActions: Runnable override val available: Boolean get() = controlsController.get().available @@ -134,9 +135,10 @@ class ControlsUiControllerImpl @Inject constructor ( } } - override fun show(parent: ViewGroup) { + override fun show(parent: ViewGroup, dismissGlobalActions: Runnable) { Log.d(ControlsUiController.TAG, "show()") this.parent = parent + this.dismissGlobalActions = dismissGlobalActions hidden = false allStructures = controlsController.get().getFavorites() @@ -169,7 +171,7 @@ class ControlsUiControllerImpl @Inject constructor ( fadeAnim.setDuration(FADE_IN_MILLIS) fadeAnim.addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - show(parent) + show(parent, dismissGlobalActions) val showAnim = ObjectAnimator.ofFloat(parent, "alpha", 0.0f, 1.0f) showAnim.setInterpolator(DecelerateInterpolator(1.0f)) showAnim.setDuration(FADE_IN_MILLIS) @@ -256,9 +258,10 @@ class ControlsUiControllerImpl @Inject constructor ( } private fun startActivity(context: Context, intent: Intent) { - val closeDialog = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) - context.sendBroadcast(closeDialog) + // Force animations when transitioning from a dialog to an activity + intent.putExtra(ControlsUiController.EXTRA_ANIMATE, true) context.startActivity(intent) + dismissGlobalActions.run() } private fun showControlsView(items: List<SelectionItem>) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java index a7c40435b3ca..8f3dc224384b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java @@ -19,8 +19,10 @@ package com.android.systemui.dagger; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; +import android.app.ActivityTaskManager; import android.app.AlarmManager; import android.app.IActivityManager; +import android.app.IActivityTaskManager; import android.app.IWallpaperManager; import android.app.KeyguardManager; import android.app.NotificationManager; @@ -128,6 +130,12 @@ public class SystemServicesModule { return ActivityManager.getService(); } + @Singleton + @Provides + static IActivityTaskManager provideIActivityTaskManager() { + return ActivityTaskManager.getService(); + } + @Provides @Singleton static IBatteryStats provideIBatteryStats() { diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index ea358c74a0e0..a24fede36e4c 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -16,6 +16,8 @@ package com.android.systemui.globalactions; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; +import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; @@ -110,6 +112,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.controls.ControlsServiceInfo; import com.android.systemui.controls.controller.ControlsController; +import com.android.systemui.controls.management.ControlsAnimations; import com.android.systemui.controls.management.ControlsListingController; import com.android.systemui.controls.ui.ControlsUiController; import com.android.systemui.dagger.qualifiers.Background; @@ -126,7 +129,6 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.EmergencyDialerConstants; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.leak.RotationUtils; -import com.android.systemui.volume.SystemUIInterpolators.LogAccelerateInterpolator; import java.util.ArrayList; import java.util.List; @@ -479,7 +481,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, */ @VisibleForTesting protected int getMaxShownPowerItems() { - if (shouldUseControlsLayout()) { + // TODO: Overflow disabled on keyguard while we solve for touch blocking issues. + if (shouldUseControlsLayout() && !mKeyguardShowing) { return mResources.getInteger(com.android.systemui.R.integer.power_menu_max_columns); } else { return Integer.MAX_VALUE; @@ -829,7 +832,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mHandler.postDelayed(new Runnable() { @Override public void run() { - mScreenshotHelper.takeScreenshot(1, true, true, mHandler, null); + mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, true, true, + SCREENSHOT_GLOBAL_ACTIONS, mHandler, null); mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU); mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS); } @@ -1815,7 +1819,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, case MESSAGE_DISMISS: if (mDialog != null) { if (SYSTEM_DIALOG_REASON_DREAM.equals(msg.obj)) { - mDialog.dismissImmediately(); + mDialog.completeDismiss(); } else { mDialog.dismiss(); } @@ -1894,6 +1898,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private ControlsUiController mControlsUiController; private ViewGroup mControlsView; + private ViewGroup mContainer; ActionsDialog(Context context, MyAdapter adapter, MyOverflowAdapter overflowAdapter, GlobalActionsPanelPlugin.PanelViewController plugin, @@ -2046,6 +2051,11 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, }); mGlobalActionsLayout.setRotationListener(this::onRotate); mGlobalActionsLayout.setAdapter(mAdapter); + mContainer = findViewById(com.android.systemui.R.id.global_actions_container); + // Some legacy dialog layouts don't have the outer container + if (mContainer == null) { + mContainer = mGlobalActionsLayout; + } mOverflowPopup = createPowerOverflowPopup(); @@ -2172,10 +2182,10 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mHadTopUi = mNotificationShadeWindowController.getForceHasTopUi(); mNotificationShadeWindowController.setForceHasTopUi(true); mBackgroundDrawable.setAlpha(0); - mGlobalActionsLayout.setTranslationX(mGlobalActionsLayout.getAnimationOffsetX()); - mGlobalActionsLayout.setTranslationY(mGlobalActionsLayout.getAnimationOffsetY()); - mGlobalActionsLayout.setAlpha(0); - mGlobalActionsLayout.animate() + mContainer.setTranslationX(mGlobalActionsLayout.getAnimationOffsetX()); + mContainer.setTranslationY(mGlobalActionsLayout.getAnimationOffsetY()); + mContainer.setAlpha(0); + mContainer.animate() .alpha(1) .translationX(0) .translationY(0) @@ -2200,48 +2210,55 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, return WindowInsets.CONSUMED; }); if (mControlsUiController != null) { - mControlsUiController.show(mControlsView); + mControlsUiController.show(mControlsView, this::dismissForControlsActivity); } } @Override public void dismiss() { + dismissWithAnimation(() -> { + mContainer.setTranslationX(0); + mContainer.setTranslationY(0); + mContainer.setAlpha(1); + mContainer.animate() + .alpha(0) + .translationX(mGlobalActionsLayout.getAnimationOffsetX()) + .translationY(mGlobalActionsLayout.getAnimationOffsetY()) + .setDuration(450) + .withEndAction(this::completeDismiss) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setUpdateListener(animation -> { + float animatedValue = 1f - animation.getAnimatedFraction(); + int alpha = (int) (animatedValue * mScrimAlpha * 255); + mBackgroundDrawable.setAlpha(alpha); + mDepthController.updateGlobalDialogVisibility(animatedValue, + mGlobalActionsLayout); + }) + .start(); + }); + } + + private void dismissForControlsActivity() { + dismissWithAnimation(() -> { + ViewGroup root = (ViewGroup) mGlobalActionsLayout.getParent(); + ControlsAnimations.exitAnimation(root, this::completeDismiss).start(); + }); + } + + void dismissWithAnimation(Runnable animation) { if (!mShowing) { return; } mShowing = false; - if (mControlsUiController != null) mControlsUiController.hide(); - mGlobalActionsLayout.setTranslationX(0); - mGlobalActionsLayout.setTranslationY(0); - mGlobalActionsLayout.setAlpha(1); - mGlobalActionsLayout.animate() - .alpha(0) - .translationX(mGlobalActionsLayout.getAnimationOffsetX()) - .translationY(mGlobalActionsLayout.getAnimationOffsetY()) - .setDuration(550) - .withEndAction(this::completeDismiss) - .setInterpolator(new LogAccelerateInterpolator()) - .setUpdateListener(animation -> { - float animatedValue = 1f - animation.getAnimatedFraction(); - int alpha = (int) (animatedValue * mScrimAlpha * 255); - mBackgroundDrawable.setAlpha(alpha); - mDepthController.updateGlobalDialogVisibility(animatedValue, - mGlobalActionsLayout); - }) - .start(); - dismissPanel(); - resetOrientation(); + animation.run(); } - void dismissImmediately() { + private void completeDismiss() { mShowing = false; - if (mControlsUiController != null) mControlsUiController.hide(); - dismissPanel(); resetOrientation(); - completeDismiss(); - } - - private void completeDismiss() { + dismissPanel(); + dismissOverflow(); + if (mControlsUiController != null) mControlsUiController.hide(); mNotificationShadeWindowController.setForceHasTopUi(mHadTopUi); mDepthController.updateGlobalDialogVisibility(0, null /* view */); super.dismiss(); @@ -2253,6 +2270,12 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, } } + private void dismissOverflow() { + if (mOverflowPopup != null) { + mOverflowPopup.dismiss(); + } + } + private void setRotationSuggestionsEnabled(boolean enabled) { try { final int userId = Binder.getCallingUserHandle().getIdentifier(); @@ -2296,7 +2319,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, initializeLayout(); mGlobalActionsLayout.updateList(); if (mControlsUiController != null) { - mControlsUiController.show(mControlsView); + mControlsUiController.show(mControlsView, this::dismissForControlsActivity); } } @@ -2335,10 +2358,9 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, && mControlsUiController.getAvailable() && !mControlsServiceInfos.isEmpty(); } - // TODO: Remove legacy layout XML and classes. protected boolean shouldUseControlsLayout() { // always use new controls layout return true; } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 14e3e9390825..123cf78d74f8 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -61,6 +61,18 @@ public class LogModule { return buffer; } + /** Provides a logging buffer for all logs related to the data layer of notifications. */ + @Provides + @Singleton + @NotifInteractionLog + public static LogBuffer provideNotifInteractionLogBuffer( + LogcatEchoTracker echoTracker, + DumpManager dumpManager) { + LogBuffer buffer = new LogBuffer("NotifInteractionLog", 50, 10, echoTracker); + buffer.attach(dumpManager); + return buffer; + } + /** Provides a logging buffer for all logs related to Quick Settings. */ @Provides @Singleton diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java new file mode 100644 index 000000000000..20fc6ff445a6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/NotifInteractionLog.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.dagger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.android.systemui.log.LogBuffer; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +/** + * A {@link LogBuffer} for messages related to the user interacting with notifications (e.g. + * clicking on them). + */ +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface NotifInteractionLog { +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 9217eb161a87..233d24b17d44 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -21,20 +21,25 @@ import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.ColorStateList; import android.graphics.Bitmap; +import android.graphics.ImageDecoder; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; +import android.media.MediaDescription; import android.media.MediaMetadata; +import android.media.ThumbnailUtils; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.PlaybackState; +import android.net.Uri; +import android.service.media.MediaBrowserService; import android.util.Log; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnAttachStateChangeListener; @@ -55,8 +60,10 @@ import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.qs.QSMediaBrowser; import com.android.systemui.util.Assert; +import java.io.IOException; import java.util.List; import java.util.concurrent.Executor; @@ -67,7 +74,7 @@ public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; @Nullable private final LocalMediaManager mLocalMediaManager; private final Executor mForegroundExecutor; - private final Executor mBackgroundExecutor; + protected final Executor mBackgroundExecutor; private Context mContext; protected LinearLayout mMediaNotifView; @@ -76,13 +83,18 @@ public class MediaControlPanel { private MediaController mController; private int mForegroundColor; private int mBackgroundColor; - protected ComponentName mRecvComponent; private MediaDevice mDevice; + protected ComponentName mServiceComponent; private boolean mIsRegistered = false; private String mKey; private final int[] mActionIds; + public static final String MEDIA_PREFERENCES = "media_control_prefs"; + public static final String MEDIA_PREFERENCE_KEY = "browser_components"; + private SharedPreferences mSharedPrefs; + private boolean mCheckedForResumption = false; + // Button IDs used in notifications protected static final int[] NOTIF_ACTION_IDS = { com.android.internal.R.id.action0, @@ -92,6 +104,13 @@ public class MediaControlPanel { com.android.internal.R.id.action4 }; + // URI fields to try loading album art from + private static final String[] ART_URIS = { + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + }; + private final MediaController.Callback mSessionCallback = new MediaController.Callback() { @Override public void onSessionDestroyed() { @@ -154,7 +173,6 @@ public class MediaControlPanel { * Initialize a new control panel * @param context * @param parent - * @param manager * @param routeManager Manager used to listen for device change events. * @param layoutId layout resource to use for this control panel * @param actionIds resource IDs for action buttons in the layout @@ -198,47 +216,52 @@ public class MediaControlPanel { /** * Update the media panel view for the given media session * @param token - * @param icon + * @param iconDrawable + * @param largeIcon * @param iconColor * @param bgColor * @param contentIntent * @param appNameString * @param key */ - public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, - int bgColor, PendingIntent contentIntent, String appNameString, String key) { - mToken = token; + public void setMediaSession(MediaSession.Token token, Drawable iconDrawable, Icon largeIcon, + int iconColor, int bgColor, PendingIntent contentIntent, String appNameString, + String key) { + // Ensure that component names are updated if token has changed + if (mToken == null || !mToken.equals(token)) { + mToken = token; + mServiceComponent = null; + mCheckedForResumption = false; + } + mForegroundColor = iconColor; mBackgroundColor = bgColor; mController = new MediaController(mContext, mToken); mKey = key; - MediaMetadata mediaMetadata = mController.getMetadata(); - - // Try to find a receiver for the media button that matches this app - PackageManager pm = mContext.getPackageManager(); - Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON); - List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser()); - if (info != null) { - for (ResolveInfo inf : info) { - if (inf.activityInfo.packageName.equals(mController.getPackageName())) { - mRecvComponent = inf.getComponentInfo().getComponentName(); + // Try to find a browser service component for this app + // TODO also check for a media button receiver intended for restarting (b/154127084) + // Only check if we haven't tried yet or the session token changed + String pkgName = mController.getPackageName(); + if (mServiceComponent == null && !mCheckedForResumption) { + Log.d(TAG, "Checking for service component"); + PackageManager pm = mContext.getPackageManager(); + Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE); + List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0); + if (resumeInfo != null) { + for (ResolveInfo inf : resumeInfo) { + if (inf.serviceInfo.packageName.equals(mController.getPackageName())) { + mBackgroundExecutor.execute(() -> + tryUpdateResumptionList(inf.getComponentInfo().getComponentName())); + break; + } } } + mCheckedForResumption = true; } mController.registerCallback(mSessionCallback); - if (mediaMetadata == null) { - Log.e(TAG, "Media metadata was null"); - return; - } - - ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); - if (albumView != null) { - // Resize art in a background thread - mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView)); - } mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); // Click action @@ -256,32 +279,9 @@ public class MediaControlPanel { // App icon ImageView appIcon = mMediaNotifView.findViewById(R.id.icon); - Drawable iconDrawable = icon.loadDrawable(mContext); iconDrawable.setTint(mForegroundColor); appIcon.setImageDrawable(iconDrawable); - // Song name - TextView titleText = mMediaNotifView.findViewById(R.id.header_title); - String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); - titleText.setText(songName); - titleText.setTextColor(mForegroundColor); - - // Not in mini player: - // App title - TextView appName = mMediaNotifView.findViewById(R.id.app_name); - if (appName != null) { - appName.setText(appNameString); - appName.setTextColor(mForegroundColor); - } - - // Artist name - TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); - if (artistText != null) { - String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); - artistText.setText(artistName); - artistText.setTextColor(mForegroundColor); - } - // Transfer chip mSeamless = mMediaNotifView.findViewById(R.id.media_seamless); if (mSeamless != null && mLocalMediaManager != null) { @@ -300,6 +300,39 @@ public class MediaControlPanel { } makeActive(); + + // App title (not in mini player) + TextView appName = mMediaNotifView.findViewById(R.id.app_name); + if (appName != null) { + appName.setText(appNameString); + appName.setTextColor(mForegroundColor); + } + + MediaMetadata mediaMetadata = mController.getMetadata(); + if (mediaMetadata == null) { + Log.e(TAG, "Media metadata was null"); + return; + } + + ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); + if (albumView != null) { + // Resize art in a background thread + mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, largeIcon, albumView)); + } + + // Song name + TextView titleText = mMediaNotifView.findViewById(R.id.header_title); + String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE); + titleText.setText(songName); + titleText.setTextColor(mForegroundColor); + + // Artist name (not in mini player) + TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); + if (artistText != null) { + String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST); + artistText.setText(artistName); + artistText.setTextColor(mForegroundColor); + } } /** @@ -320,9 +353,12 @@ public class MediaControlPanel { /** * Get the name of the package associated with the current media controller - * @return the package name + * @return the package name, or null if no controller */ public String getMediaPlayerPackage() { + if (mController == null) { + return null; + } return mController.getPackageName(); } @@ -370,18 +406,86 @@ public class MediaControlPanel { /** * Process album art for layout + * @param description media description + * @param albumView view to hold the album art + */ + protected void processAlbumArt(MediaDescription description, ImageView albumView) { + Bitmap albumArt = null; + + // First try loading from URI + albumArt = loadBitmapFromUri(description.getIconUri()); + + // Then check bitmap + if (albumArt == null) { + albumArt = description.getIconBitmap(); + } + + processAlbumArtInternal(albumArt, albumView); + } + + /** + * Process album art for layout * @param metadata media metadata + * @param largeIcon from notification, checked as a fallback if metadata does not have art * @param albumView view to hold the album art */ - private void processAlbumArt(MediaMetadata metadata, ImageView albumView) { - Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); + private void processAlbumArt(MediaMetadata metadata, Icon largeIcon, ImageView albumView) { + Bitmap albumArt = null; + + // First look in URI fields + for (String field : ART_URIS) { + String uriString = metadata.getString(field); + if (uriString != null) { + albumArt = loadBitmapFromUri(Uri.parse(uriString)); + if (albumArt != null) { + Log.d(TAG, "loaded art from " + field); + break; + } + } + } + + // Then check bitmap field + if (albumArt == null) { + albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); + } + + // Finally try the notification's largeIcon + if (albumArt == null && largeIcon != null) { + albumArt = largeIcon.getBitmap(); + } + + processAlbumArtInternal(albumArt, albumView); + } + + /** + * Load a bitmap from a URI + * @param uri + * @return bitmap, or null if couldn't be loaded + */ + private Bitmap loadBitmapFromUri(Uri uri) { + ImageDecoder.Source source = ImageDecoder.createSource(mContext.getContentResolver(), uri); + try { + return ImageDecoder.decodeBitmap(source); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Resize and crop the image if provided and update the control view + * @param albumArt Bitmap of art to display, or null to hide view + * @param albumView View that will hold the art + */ + private void processAlbumArtInternal(@Nullable Bitmap albumArt, ImageView albumView) { + // Resize RoundedBitmapDrawable roundedDrawable = null; if (albumArt != null) { + float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius); Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true); int albumSize = (int) mContext.getResources().getDimension( R.dimen.qs_media_album_size); - Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false); + Bitmap scaled = ThumbnailUtils.extractThumbnail(original, albumSize, albumSize); roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled); roundedDrawable.setCornerRadius(radius); } else { @@ -449,10 +553,24 @@ public class MediaControlPanel { } /** - * Put controls into a resumption state + * Puts controls into a resumption state if possible, or calls removePlayer if no component was + * found that could resume playback */ public void clearControls() { Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage()); + if (mServiceComponent == null) { + // If we don't have a way to resume, just remove the player altogether + Log.d(TAG, "Removing unresumable controls"); + removePlayer(); + return; + } + resetButtons(); + } + + /** + * Hide the media buttons and show only a restart button + */ + protected void resetButtons() { // Hide all the old buttons for (int i = 0; i < mActionIds.length; i++) { ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]); @@ -465,27 +583,8 @@ public class MediaControlPanel { ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]); btn.setOnClickListener(v -> { Log.d(TAG, "Attempting to restart session"); - // Send a media button event to previously found receiver - if (mRecvComponent != null) { - Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); - intent.setComponent(mRecvComponent); - int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY; - intent.putExtra( - Intent.EXTRA_KEY_EVENT, - new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); - mContext.sendBroadcast(intent); - } else { - // If we don't have a receiver, try relaunching the activity instead - if (mController.getSessionActivity() != null) { - try { - mController.getSessionActivity().send(); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent was canceled", e); - } - } else { - Log.e(TAG, "No receiver or activity to restart"); - } - } + QSMediaBrowser browser = new QSMediaBrowser(mContext, null, mServiceComponent); + browser.restart(); }); btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play)); btn.setImageTintList(ColorStateList.valueOf(mForegroundColor)); @@ -514,4 +613,65 @@ public class MediaControlPanel { } } + /** + * Verify that we can connect to the given component with a MediaBrowser, and if so, add that + * component to the list of resumption components + */ + private void tryUpdateResumptionList(ComponentName componentName) { + Log.d(TAG, "Testing if we can connect to " + componentName); + QSMediaBrowser.testConnection(mContext, + new QSMediaBrowser.Callback() { + @Override + public void onConnected() { + Log.d(TAG, "yes we can resume with " + componentName); + mServiceComponent = componentName; + updateResumptionList(componentName); + } + + @Override + public void onError() { + Log.d(TAG, "Cannot resume with " + componentName); + mServiceComponent = null; + clearControls(); + // remove + } + }, + componentName); + } + + /** + * Add the component to the saved list of media browser services, checking for duplicates and + * removing older components that exceed the maximum limit + * @param componentName + */ + private synchronized void updateResumptionList(ComponentName componentName) { + // Add to front of saved list + if (mSharedPrefs == null) { + mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0); + } + String componentString = componentName.flattenToString(); + String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null); + if (listString == null) { + listString = componentString; + } else { + String[] components = listString.split(QSMediaBrowser.DELIMITER); + StringBuilder updated = new StringBuilder(componentString); + int nBrowsers = 1; + for (int i = 0; i < components.length + && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) { + if (componentString.equals(components[i])) { + continue; + } + updated.append(QSMediaBrowser.DELIMITER).append(components[i]); + nBrowsers++; + } + listString = updated.toString(); + } + mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply(); + } + + /** + * Called when a player can't be resumed to give it an opportunity to hide or remove itself + */ + protected void removePlayer() { } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java new file mode 100644 index 000000000000..302b84203641 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.MediaDescription; +import android.media.browse.MediaBrowser; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.os.Bundle; +import android.service.media.MediaBrowserService; +import android.util.Log; + +import java.util.List; + +/** + * Media browser for managing resumption in QS media controls + */ +public class QSMediaBrowser { + + /** Maximum number of controls to show on boot */ + public static final int MAX_RESUMPTION_CONTROLS = 5; + + /** Delimiter for saved component names */ + public static final String DELIMITER = ":"; + + private static final String TAG = "QSMediaBrowser"; + private final Context mContext; + private final Callback mCallback; + private MediaBrowser mMediaBrowser; + private ComponentName mComponentName; + + /** + * Initialize a new media browser + * @param context the context + * @param callback used to report media items found + * @param componentName Component name of the MediaBrowserService this browser will connect to + */ + public QSMediaBrowser(Context context, Callback callback, ComponentName componentName) { + mContext = context; + mCallback = callback; + mComponentName = componentName; + + Bundle rootHints = new Bundle(); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + mMediaBrowser = new MediaBrowser(mContext, + mComponentName, + mConnectionCallback, + rootHints); + } + + /** + * Connects to the MediaBrowserService and looks for valid media. If a media item is returned + * by the service, QSMediaBrowser.Callback#addTrack will be called with its MediaDescription + */ + public void findRecentMedia() { + Log.d(TAG, "Connecting to " + mComponentName); + mMediaBrowser.connect(); + } + + private final MediaBrowser.SubscriptionCallback mSubscriptionCallback = + new MediaBrowser.SubscriptionCallback() { + @Override + public void onChildrenLoaded(String parentId, + List<MediaBrowser.MediaItem> children) { + if (children.size() == 0) { + Log.e(TAG, "No children found"); + return; + } + // We ask apps to return a playable item as the first child when sending + // a request with EXTRA_RECENT; if they don't, no resume controls + MediaBrowser.MediaItem child = children.get(0); + MediaDescription desc = child.getDescription(); + if (child.isPlayable()) { + mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this); + } else { + Log.e(TAG, "Child found but not playable for " + mComponentName); + } + mMediaBrowser.disconnect(); + } + + @Override + public void onError(String parentId) { + Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId); + mMediaBrowser.disconnect(); + } + + @Override + public void onError(String parentId, Bundle options) { + Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId + + ", options: " + options); + mMediaBrowser.disconnect(); + } + }; + + private final MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + /** + * Invoked after {@link MediaBrowser#connect()} when the request has successfully completed. + * For resumption controls, apps are expected to return a playable media item as the first + * child. If there are no children or it isn't playable it will be ignored. + */ + @Override + public void onConnected() { + if (mMediaBrowser.isConnected()) { + mCallback.onConnected(); + Log.d(TAG, "Service connected for " + mComponentName); + String root = mMediaBrowser.getRoot(); + mMediaBrowser.subscribe(root, mSubscriptionCallback); + } + } + + /** + * Invoked when the client is disconnected from the media browser. + */ + @Override + public void onConnectionSuspended() { + Log.d(TAG, "Connection suspended for " + mComponentName); + } + + /** + * Invoked when the connection to the media browser failed. + */ + @Override + public void onConnectionFailed() { + Log.e(TAG, "Connection failed for " + mComponentName); + mCallback.onError(); + } + }; + + /** + * Connects to the MediaBrowserService and starts playback + */ + public void restart() { + if (mMediaBrowser.isConnected()) { + mMediaBrowser.disconnect(); + } + Bundle rootHints = new Bundle(); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + mMediaBrowser = new MediaBrowser(mContext, mComponentName, + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + Log.d(TAG, "Connected for restart " + mMediaBrowser.isConnected()); + MediaSession.Token token = mMediaBrowser.getSessionToken(); + MediaController controller = new MediaController(mContext, token); + controller.getTransportControls(); + controller.getTransportControls().prepare(); + controller.getTransportControls().play(); + } + }, rootHints); + mMediaBrowser.connect(); + } + + /** + * Get the media session token + * @return the token, or null if the MediaBrowser is null or disconnected + */ + public MediaSession.Token getToken() { + if (mMediaBrowser == null || !mMediaBrowser.isConnected()) { + return null; + } + return mMediaBrowser.getSessionToken(); + } + + /** + * Get an intent to launch the app associated with this browser service + * @return + */ + public PendingIntent getAppIntent() { + PackageManager pm = mContext.getPackageManager(); + Intent launchIntent = pm.getLaunchIntentForPackage(mComponentName.getPackageName()); + return PendingIntent.getActivity(mContext, 0, launchIntent, 0); + } + + /** + * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser + * @param mContext the context + * @param callback methods onConnected or onError will be called to indicate whether the + * connection was successful or not + * @param mComponentName Component name of the MediaBrowserService this browser will connect to + */ + public static MediaBrowser testConnection(Context mContext, Callback callback, + ComponentName mComponentName) { + final MediaBrowser.ConnectionCallback mConnectionCallback = + new MediaBrowser.ConnectionCallback() { + @Override + public void onConnected() { + Log.d(TAG, "connected"); + callback.onConnected(); + } + + @Override + public void onConnectionSuspended() { + Log.d(TAG, "suspended"); + callback.onError(); + } + + @Override + public void onConnectionFailed() { + Log.d(TAG, "failed"); + callback.onError(); + } + }; + Bundle rootHints = new Bundle(); + rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); + MediaBrowser browser = new MediaBrowser(mContext, + mComponentName, + mConnectionCallback, + rootHints); + browser.connect(); + return browser; + } + + /** + * Interface to handle results from QSMediaBrowser + */ + public static class Callback { + /** + * Called when the browser has successfully connected to the service + */ + public void onConnected() { + } + + /** + * Called when the browser encountered an error connecting to the service + */ + public void onError() { + } + + /** + * Called when the browser finds a suitable track to add to the media carousel + * @param track media info for the item + * @param component component of the MediaBrowserService which returned this + * @param browser reference to the browser + */ + public void addTrack(MediaDescription track, ComponentName component, + QSMediaBrowser browser) { + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java index 89b22bc518bb..9e574a1fa621 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java @@ -18,11 +18,13 @@ package com.android.systemui.qs; import static com.android.systemui.util.SysuiLifecycle.viewAttachLifecycle; -import android.app.Notification; +import android.app.PendingIntent; import android.content.Context; +import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.media.MediaDescription; import android.media.session.MediaController; import android.media.session.MediaSession; import android.util.Log; @@ -60,9 +62,11 @@ public class QSMediaPlayer extends MediaControlPanel { }; private final QSPanel mParent; + private final Executor mForegroundExecutor; private final DelayableExecutor mBackgroundExecutor; private final SeekBarViewModel mSeekBarViewModel; private final SeekBarObserver mSeekBarObserver; + private String mPackageName; /** * Initialize quick shade version of player @@ -77,6 +81,7 @@ public class QSMediaPlayer extends MediaControlPanel { super(context, parent, routeManager, R.layout.qs_media_panel, QS_ACTION_IDS, foregroundExecutor, backgroundExecutor); mParent = (QSPanel) parent; + mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); mSeekBarObserver = new SeekBarObserver(getView()); @@ -90,47 +95,103 @@ public class QSMediaPlayer extends MediaControlPanel { } /** + * Add a media panel view based on a media description. Used for resumption + * @param description + * @param iconColor + * @param bgColor + * @param contentIntent + * @param pkgName + */ + public void setMediaSession(MediaSession.Token token, MediaDescription description, + int iconColor, int bgColor, PendingIntent contentIntent, String pkgName) { + mPackageName = pkgName; + PackageManager pm = getContext().getPackageManager(); + Drawable icon = null; + CharSequence appName = pkgName.substring(pkgName.lastIndexOf(".")); + try { + icon = pm.getApplicationIcon(pkgName); + appName = pm.getApplicationLabel(pm.getApplicationInfo(pkgName, 0)); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Error getting package information", e); + } + + // Set what we can normally + super.setMediaSession(token, icon, null, iconColor, bgColor, contentIntent, + appName.toString(), null); + + // Then add info from MediaDescription + ImageView albumView = mMediaNotifView.findViewById(R.id.album_art); + if (albumView != null) { + // Resize art in a background thread + mBackgroundExecutor.execute(() -> processAlbumArt(description, albumView)); + } + + // Song name + TextView titleText = mMediaNotifView.findViewById(R.id.header_title); + CharSequence songName = description.getTitle(); + titleText.setText(songName); + titleText.setTextColor(iconColor); + + // Artist name (not in mini player) + TextView artistText = mMediaNotifView.findViewById(R.id.header_artist); + if (artistText != null) { + CharSequence artistName = description.getSubtitle(); + artistText.setText(artistName); + artistText.setTextColor(iconColor); + } + + initLongPressMenu(iconColor); + + // Set buttons to resume state + resetButtons(); + } + + /** * Update media panel view for the given media session * @param token token for this media session * @param icon app notification icon + * @param largeIcon notification's largeIcon, used as a fallback for album art * @param iconColor foreground color (for text, icons) * @param bgColor background color * @param actionsContainer a LinearLayout containing the media action buttons - * @param notif reference to original notification + * @param contentIntent Intent to send when user taps on player + * @param appName Application title * @param key original notification's key */ - public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, - int bgColor, View actionsContainer, Notification notif, String key) { + public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, + int iconColor, int bgColor, View actionsContainer, PendingIntent contentIntent, + String appName, String key) { - String appName = Notification.Builder.recoverBuilder(getContext(), notif) - .loadHeaderAppName(); - super.setMediaSession(token, icon, iconColor, bgColor, notif.contentIntent, appName, key); + super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, appName, + key); // Media controls - LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; - int i = 0; - for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); - if (thatBtn == null || thatBtn.getDrawable() == null - || thatBtn.getVisibility() != View.VISIBLE) { - thisBtn.setVisibility(View.GONE); - continue; - } + if (actionsContainer != null) { + LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; + int i = 0; + for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) { + ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); + ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]); + if (thatBtn == null || thatBtn.getDrawable() == null + || thatBtn.getVisibility() != View.VISIBLE) { + thisBtn.setVisibility(View.GONE); + continue; + } - Drawable thatIcon = thatBtn.getDrawable(); - thisBtn.setImageDrawable(thatIcon.mutate()); - thisBtn.setVisibility(View.VISIBLE); - thisBtn.setOnClickListener(v -> { - Log.d(TAG, "clicking on other button"); - thatBtn.performClick(); - }); - } + Drawable thatIcon = thatBtn.getDrawable(); + thisBtn.setImageDrawable(thatIcon.mutate()); + thisBtn.setVisibility(View.VISIBLE); + thisBtn.setOnClickListener(v -> { + Log.d(TAG, "clicking on other button"); + thatBtn.performClick(); + }); + } - // Hide any unused buttons - for (; i < QS_ACTION_IDS.length; i++) { - ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); - thisBtn.setVisibility(View.GONE); + // Hide any unused buttons + for (; i < QS_ACTION_IDS.length; i++) { + ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]); + thisBtn.setVisibility(View.GONE); + } } // Seek Bar @@ -138,6 +199,10 @@ public class QSMediaPlayer extends MediaControlPanel { mBackgroundExecutor.execute( () -> mSeekBarViewModel.updateController(controller, iconColor)); + initLongPressMenu(iconColor); + } + + private void initLongPressMenu(int iconColor) { // Set up long press menu View guts = mMediaNotifView.findViewById(R.id.media_guts); View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); @@ -145,7 +210,7 @@ public class QSMediaPlayer extends MediaControlPanel { View clearView = options.findViewById(R.id.remove); clearView.setOnClickListener(b -> { - mParent.removeMediaPlayer(QSMediaPlayer.this); + removePlayer(); }); ImageView removeIcon = options.findViewById(R.id.remove_icon); removeIcon.setImageTintList(ColorStateList.valueOf(iconColor)); @@ -165,11 +230,9 @@ public class QSMediaPlayer extends MediaControlPanel { } @Override - public void clearControls() { - super.clearControls(); - + protected void resetButtons() { + super.resetButtons(); mSeekBarViewModel.clearController(); - View guts = mMediaNotifView.findViewById(R.id.media_guts); View options = mMediaNotifView.findViewById(R.id.qs_media_controls_options); @@ -192,4 +255,19 @@ public class QSMediaPlayer extends MediaControlPanel { public void setListening(boolean listening) { mSeekBarViewModel.setListening(listening); } + + @Override + public void removePlayer() { + Log.d(TAG, "removing player from parent: " + mParent); + // Ensure this happens on the main thread (could happen in QSMediaBrowser callback) + mForegroundExecutor.execute(() -> mParent.removeMediaPlayer(QSMediaPlayer.this)); + } + + @Override + public String getMediaPlayerPackage() { + if (getController() == null) { + return mPackageName; + } + return super.getMediaPlayerPackage(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index fee08389388d..1252008755a7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -21,16 +21,26 @@ import static com.android.systemui.util.InjectionInflationController.VIEW_CONTEX import static com.android.systemui.util.Utils.useQsMediaPlayer; import android.annotation.Nullable; +import android.app.Notification; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; +import android.media.MediaDescription; import android.media.session.MediaSession; import android.metrics.LogMaker; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.UserHandle; +import android.os.UserManager; import android.service.notification.StatusBarNotification; import android.service.quicksettings.Tile; import android.util.AttributeSet; @@ -54,6 +64,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.media.MediaControlPanel; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTileView; @@ -90,6 +101,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne protected final Context mContext; protected final ArrayList<TileRecord> mRecords = new ArrayList<>(); + private final BroadcastDispatcher mBroadcastDispatcher; private String mCachedSpecs = ""; protected final View mBrightnessView; private final H mHandler = new H(); @@ -123,6 +135,19 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private BrightnessMirrorController mBrightnessMirrorController; private View mDivider; + private boolean mHasLoadedMediaControls; + + private final BroadcastReceiver mUserChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_USER_UNLOCKED.equals(action)) { + if (!mHasLoadedMediaControls) { + loadMediaResumptionControls(); + } + } + } + }; @Inject public QSPanel( @@ -142,6 +167,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mForegroundExecutor = foregroundExecutor; mBackgroundExecutor = backgroundExecutor; mLocalBluetoothManager = localBluetoothManager; + mBroadcastDispatcher = broadcastDispatcher; setOrientation(VERTICAL); @@ -176,7 +202,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne updateResources(); mBrightnessController = new BrightnessController(getContext(), - findViewById(R.id.brightness_slider), broadcastDispatcher); + findViewById(R.id.brightness_slider), mBroadcastDispatcher); } @Override @@ -200,14 +226,16 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne * Add or update a player for the associated media session * @param token * @param icon + * @param largeIcon * @param iconColor * @param bgColor * @param actionsContainer * @param notif * @param key */ - public void addMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor, - View actionsContainer, StatusBarNotification notif, String key) { + public void addMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, + int iconColor, int bgColor, View actionsContainer, StatusBarNotification notif, + String key) { if (!useQsMediaPlayer(mContext)) { // Shouldn't happen, but just in case Log.e(TAG, "Tried to add media session without player!"); @@ -221,7 +249,14 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne QSMediaPlayer player = null; String packageName = notif.getPackageName(); for (QSMediaPlayer p : mMediaPlayers) { - if (p.getMediaSessionToken().equals(token)) { + if (p.getKey() == null) { + // No notification key = loaded via mediabrowser, so just match on package + if (packageName.equals(p.getMediaPlayerPackage())) { + Log.d(TAG, "Found matching resume player by package: " + packageName); + player = p; + break; + } + } else if (p.getMediaSessionToken().equals(token)) { Log.d(TAG, "Found matching player by token " + packageName); player = p; break; @@ -262,8 +297,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne } Log.d(TAG, "setting player session"); - player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer, - notif.getNotification(), key); + String appName = Notification.Builder.recoverBuilder(getContext(), notif.getNotification()) + .loadHeaderAppName(); + player.setMediaSession(token, icon, largeIcon, iconColor, bgColor, actionsContainer, + notif.getNotification().contentIntent, appName, key); if (mMediaPlayers.size() > 0) { ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE); @@ -293,6 +330,74 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne return true; } + private final QSMediaBrowser.Callback mMediaBrowserCallback = new QSMediaBrowser.Callback() { + @Override + public void addTrack(MediaDescription desc, ComponentName component, + QSMediaBrowser browser) { + if (component == null) { + Log.e(TAG, "Component cannot be null"); + return; + } + + Log.d(TAG, "adding track from browser: " + desc + ", " + component); + QSMediaPlayer player = new QSMediaPlayer(mContext, QSPanel.this, + null, mForegroundExecutor, mBackgroundExecutor); + + String pkgName = component.getPackageName(); + + // Add controls to carousel + int playerWidth = (int) getResources().getDimension(R.dimen.qs_media_width); + int padding = (int) getResources().getDimension(R.dimen.qs_media_padding); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(playerWidth, + LayoutParams.MATCH_PARENT); + lp.setMarginStart(padding); + lp.setMarginEnd(padding); + mMediaCarousel.addView(player.getView(), lp); + ((View) mMediaCarousel.getParent()).setVisibility(View.VISIBLE); + mMediaPlayers.add(player); + + int iconColor = Color.DKGRAY; + int bgColor = Color.LTGRAY; + + MediaSession.Token token = browser.getToken(); + player.setMediaSession(token, desc, iconColor, bgColor, browser.getAppIntent(), + pkgName); + } + }; + + /** + * Load controls for resuming media, if available + */ + private void loadMediaResumptionControls() { + if (!useQsMediaPlayer(mContext)) { + return; + } + Log.d(TAG, "Loading resumption controls"); + + // Look up saved components to resume + Context userContext = mContext.createContextAsUser(mContext.getUser(), 0); + SharedPreferences prefs = userContext.getSharedPreferences( + MediaControlPanel.MEDIA_PREFERENCES, Context.MODE_PRIVATE); + String listString = prefs.getString(MediaControlPanel.MEDIA_PREFERENCE_KEY, null); + if (listString == null) { + Log.d(TAG, "No saved media components"); + return; + } + + String[] components = listString.split(QSMediaBrowser.DELIMITER); + Log.d(TAG, "components are: " + listString + " count " + components.length); + for (int i = 0; i < components.length && i < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) { + String[] info = components[i].split("/"); + String packageName = info[0]; + String className = info[1]; + ComponentName component = new ComponentName(packageName, className); + QSMediaBrowser browser = new QSMediaBrowser(mContext, mMediaBrowserCallback, + component); + browser.findRecentMedia(); + } + mHasLoadedMediaControls = true; + } + protected void addDivider() { mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false); mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(), @@ -343,6 +448,22 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mBrightnessMirrorController.addCallback(this); } mDumpManager.registerDumpable(getDumpableTag(), this); + + if (getClass() == QSPanel.class) { + //TODO(ethibodeau) remove class check after media refactor in ag/11059751 + // Only run this in QSPanel proper, not QQS + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_USER_UNLOCKED); + mBroadcastDispatcher.registerReceiver(mUserChangeReceiver, filter, null, + UserHandle.ALL); + mHasLoadedMediaControls = false; + + UserManager userManager = mContext.getSystemService(UserManager.class); + if (userManager.isUserUnlocked(mContext.getUserId())) { + // If it's already unlocked (like if dark theme was toggled), we can load now + loadMediaResumptionControls(); + } + } } @Override @@ -358,6 +479,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mBrightnessMirrorController.removeCallback(this); } mDumpManager.unregisterDumpable(getDumpableTag()); + mBroadcastDispatcher.unregisterReceiver(mUserChangeReceiver); super.onDetachedFromWindow(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java index 62296720213b..89b36da0c834 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java @@ -59,6 +59,7 @@ public class QuickQSMediaPlayer extends MediaControlPanel { * Update media panel view for the given media session * @param token token for this media session * @param icon app notification icon + * @param largeIcon notification's largeIcon, used as a fallback for album art * @param iconColor foreground color (for text, icons) * @param bgColor background color * @param actionsContainer a LinearLayout containing the media action buttons @@ -67,8 +68,9 @@ public class QuickQSMediaPlayer extends MediaControlPanel { * @param contentIntent Intent to send when user taps on the view * @param key original notification's key */ - public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor, - View actionsContainer, int[] actionsToShow, PendingIntent contentIntent, String key) { + public void setMediaSession(MediaSession.Token token, Drawable icon, Icon largeIcon, + int iconColor, int bgColor, View actionsContainer, int[] actionsToShow, + PendingIntent contentIntent, String key) { // Only update if this is a different session and currently playing String oldPackage = ""; if (getController() != null) { @@ -83,7 +85,7 @@ public class QuickQSMediaPlayer extends MediaControlPanel { return; } - super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, null, key); + super.setMediaSession(token, icon, largeIcon, iconColor, bgColor, contentIntent, null, key); LinearLayout parentActionsLayout = (LinearLayout) actionsContainer; int i = 0; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index 9cee7e7ccba4..1a6a104387ac 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -18,7 +18,9 @@ package com.android.systemui.qs.tiles; import android.content.Intent; import android.service.quicksettings.Tile; +import android.text.TextUtils; import android.util.Log; +import android.widget.Switch; import com.android.systemui.R; import com.android.systemui.plugins.qs.QSTile; @@ -88,6 +90,10 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> state.icon = ResourceIcon.get(R.drawable.ic_qs_screenrecord); state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_start); } + state.contentDescription = TextUtils.isEmpty(state.secondaryLabel) + ? state.label + : TextUtils.concat(state.label, ", ", state.secondaryLabel); + state.expandedAccessibilityClassName = Switch.class.getName(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 66bc177da81d..fecb7b602012 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -20,6 +20,7 @@ import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_INPUT_MONITOR; @@ -380,7 +381,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis public void handleImageAsScreenshot(Bitmap screenImage, Rect locationInScreen, Insets visibleInsets, int taskId) { mScreenshotHelper.provideScreenshot(screenImage, locationInScreen, visibleInsets, - taskId, mHandler, null); + taskId, SCREENSHOT_OVERVIEW, mHandler, null); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index e1cc0b0c90c2..1efe663ca6ce 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -76,6 +76,7 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Toast; +import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -104,7 +105,6 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset */ static class SaveImageInBackgroundData { public Bitmap image; - public Uri imageUri; public Consumer<Uri> finisher; public GlobalScreenshot.ActionsReadyListener mActionsReadyListener; public int errorMsgResId; @@ -112,13 +112,33 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset void clearImage() { image = null; - imageUri = null; + } + } + + /** + * Structure returned by the SaveImageInBackgroundTask + */ + static class SavedImageData { + public Uri uri; + public Notification.Action shareAction; + public Notification.Action editAction; + public Notification.Action deleteAction; + public List<Notification.Action> smartActions; + + /** + * Used to reset the return data on error + */ + public void reset() { + uri = null; + shareAction = null; + editAction = null; + deleteAction = null; + smartActions = null; } } abstract static class ActionsReadyListener { - abstract void onActionsReady(Uri imageUri, List<Notification.Action> smartActions, - List<Notification.Action> actions); + abstract void onActionsReady(SavedImageData imageData); } // These strings are used for communicating the action invoked to @@ -147,6 +167,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset private static final int MESSAGE_CORNER_TIMEOUT = 2; private final ScreenshotNotificationsController mNotificationsController; + private final UiEventLogger mUiEventLogger; private final Context mContext; private final WindowManager mWindowManager; @@ -185,6 +206,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_CORNER_TIMEOUT: + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT); GlobalScreenshot.this.clearScreenshot("timeout"); break; default: @@ -199,9 +221,11 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset @Inject public GlobalScreenshot( Context context, @Main Resources resources, LayoutInflater layoutInflater, - ScreenshotNotificationsController screenshotNotificationsController) { + ScreenshotNotificationsController screenshotNotificationsController, + UiEventLogger uiEventLogger) { mContext = context; mNotificationsController = screenshotNotificationsController; + mUiEventLogger = uiEventLogger; // Inflate the screenshot layout mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null); @@ -222,7 +246,10 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mBackgroundProtection = mScreenshotLayout.findViewById( R.id.global_screenshot_actions_background); mDismissButton = mScreenshotLayout.findViewById(R.id.global_screenshot_dismiss_button); - mDismissButton.setOnClickListener(view -> clearScreenshot("dismiss_button")); + mDismissButton.setOnClickListener(view -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL); + clearScreenshot("dismiss_button"); + }); mScreenshotFlash = mScreenshotLayout.findViewById(R.id.global_screenshot_flash); mScreenshotSelectorView = mScreenshotLayout.findViewById(R.id.global_screenshot_selector); @@ -445,12 +472,14 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { @Override - void onActionsReady(Uri uri, List<Notification.Action> smartActions, - List<Notification.Action> actions) { - if (uri == null) { + void onActionsReady(SavedImageData imageData) { + finisher.accept(imageData.uri); + if (imageData.uri == null) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); mNotificationsController.notifyScreenshotError( R.string.screenshot_failed_to_capture_text); } else { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); mScreenshotHandler.post(() -> { if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { mScreenshotAnimation.addListener( @@ -458,13 +487,12 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - createScreenshotActionsShadeAnimation( - smartActions, actions).start(); + createScreenshotActionsShadeAnimation(imageData) + .start(); } }); } else { - createScreenshotActionsShadeAnimation(smartActions, - actions).start(); + createScreenshotActionsShadeAnimation(imageData).start(); } }); } @@ -567,8 +595,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset return dropInAnimation; } - private ValueAnimator createScreenshotActionsShadeAnimation( - List<Notification.Action> smartActions, List<Notification.Action> actions) { + private ValueAnimator createScreenshotActionsShadeAnimation(SavedImageData imageData) { LayoutInflater inflater = LayoutInflater.from(mContext); mActionsView.removeAllViews(); mActionsContainer.setScrollX(0); @@ -583,45 +610,63 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } catch (RemoteException e) { } - for (Notification.Action smartAction : smartActions) { + for (Notification.Action smartAction : imageData.smartActions) { ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( R.layout.global_screenshot_action_chip, mActionsView, false); actionChip.setText(smartAction.title); actionChip.setIcon(smartAction.getIcon(), false); actionChip.setPendingIntent(smartAction.actionIntent, - () -> clearScreenshot("chip tapped")); + () -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED); + clearScreenshot("chip tapped"); + }); mActionsView.addView(actionChip); } - for (Notification.Action action : actions) { - ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate( - R.layout.global_screenshot_action_chip, mActionsView, false); - actionChip.setText(action.title); - actionChip.setIcon(action.getIcon(), true); - actionChip.setPendingIntent(action.actionIntent, () -> clearScreenshot("chip tapped")); - if (action.actionIntent.getIntent().getAction().equals(Intent.ACTION_EDIT)) { - mScreenshotView.setOnClickListener(v -> { - try { - action.actionIntent.send(); - clearScreenshot("screenshot preview tapped"); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Intent cancelled", e); - } - }); - mScreenshotView.setContentDescription(action.title); + ScreenshotActionChip shareChip = (ScreenshotActionChip) inflater.inflate( + R.layout.global_screenshot_action_chip, mActionsView, false); + shareChip.setText(imageData.shareAction.title); + shareChip.setIcon(imageData.shareAction.getIcon(), true); + shareChip.setPendingIntent(imageData.shareAction.actionIntent, () -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED); + clearScreenshot("chip tapped"); + }); + mActionsView.addView(shareChip); + + ScreenshotActionChip editChip = (ScreenshotActionChip) inflater.inflate( + R.layout.global_screenshot_action_chip, mActionsView, false); + editChip.setText(imageData.editAction.title); + editChip.setIcon(imageData.editAction.getIcon(), true); + editChip.setPendingIntent(imageData.editAction.actionIntent, () -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED); + clearScreenshot("chip tapped"); + }); + mActionsView.addView(editChip); + + mScreenshotView.setOnClickListener(v -> { + try { + imageData.editAction.actionIntent.send(); + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED); + clearScreenshot("screenshot preview tapped"); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Intent cancelled", e); } - mActionsView.addView(actionChip); - } + }); + mScreenshotView.setContentDescription(imageData.editAction.title); + if (DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI, SCREENSHOT_SCROLLING_ENABLED, false)) { ScreenshotActionChip scrollChip = (ScreenshotActionChip) inflater.inflate( R.layout.global_screenshot_action_chip, mActionsView, false); Toast scrollNotImplemented = Toast.makeText( mContext, "Not implemented", Toast.LENGTH_SHORT); - scrollChip.setText("Extend"); // TODO (mkephart): add resource and translate + scrollChip.setText("Extend"); // TODO : add resource and translate scrollChip.setIcon( Icon.createWithResource(mContext, R.drawable.ic_arrow_downward), true); - scrollChip.setOnClickListener(v -> scrollNotImplemented.show()); + scrollChip.setOnClickListener(v -> { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SCROLL_TAPPED); + scrollNotImplemented.show(); + }); mActionsView.addView(scrollChip); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java index f3614ffbdb1b..095c32f4a2ce 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshotLegacy.java @@ -24,7 +24,6 @@ import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.annotation.Nullable; -import android.app.Notification; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -53,7 +52,6 @@ import android.widget.Toast; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; -import java.util.List; import java.util.function.Consumer; import javax.inject.Inject; @@ -347,14 +345,13 @@ public class GlobalScreenshotLegacy { // Save the screenshot once we have a bit of time now saveScreenshotInWorkerThread(finisher, new GlobalScreenshot.ActionsReadyListener() { @Override - void onActionsReady(Uri uri, List<Notification.Action> smartActions, - List<Notification.Action> actions) { - if (uri == null) { + void onActionsReady(GlobalScreenshot.SavedImageData actionData) { + if (actionData.uri == null) { mNotificationsController.notifyScreenshotError( R.string.screenshot_failed_to_capture_text); } else { mNotificationsController - .showScreenshotActionsNotification(uri, smartActions, actions); + .showScreenshotActionsNotification(actionData); } } }); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index c828c4cccce5..170174deaeb3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -83,6 +83,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final Context mContext; private final GlobalScreenshot.SaveImageInBackgroundData mParams; + private final GlobalScreenshot.SavedImageData mImageData; private final String mImageFileName; private final long mImageTime; private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider; @@ -93,6 +94,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data) { mContext = context; + mImageData = new GlobalScreenshot.SavedImageData(); // Prepare all the output metadata mParams = data; @@ -145,6 +147,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { values.put(MediaColumns.IS_PENDING, 1); final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + try { // First, write the actual data for our screenshot try (OutputStream out = resolver.openOutputStream(uri)) { @@ -192,8 +195,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { throw e; } - List<Notification.Action> actions = - populateNotificationActions(mContext, r, uri); List<Notification.Action> smartActions = new ArrayList<>(); if (mSmartActionsEnabled) { int timeoutMs = DeviceConfig.getInt( @@ -206,8 +207,14 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { mSmartActionsProvider), mContext)); } - mParams.mActionsReadyListener.onActionsReady(uri, smartActions, actions); - mParams.imageUri = uri; + + mImageData.uri = uri; + mImageData.smartActions = smartActions; + mImageData.shareAction = createShareAction(mContext, mContext.getResources(), uri); + mImageData.editAction = createEditAction(mContext, mContext.getResources(), uri); + mImageData.deleteAction = createDeleteAction(mContext, mContext.getResources(), uri); + + mParams.mActionsReadyListener.onActionsReady(mImageData); mParams.image = null; mParams.errorMsgResId = 0; } catch (Exception e) { @@ -216,29 +223,26 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Slog.e(TAG, "unable to save screenshot", e); mParams.clearImage(); mParams.errorMsgResId = R.string.screenshot_failed_to_save_text; - mParams.mActionsReadyListener.onActionsReady(null, null, null); + mImageData.reset(); + mParams.mActionsReadyListener.onActionsReady(mImageData); } return null; } @Override - protected void onPostExecute(Void params) { - mParams.finisher.accept(mParams.imageUri); - } - - @Override protected void onCancelled(Void params) { // If we are cancelled while the task is running in the background, we may get null // params. The finisher is expected to always be called back, so just use the baked-in // params from the ctor in any case. - mParams.mActionsReadyListener.onActionsReady(null, null, null); + mImageData.reset(); + mParams.mActionsReadyListener.onActionsReady(mImageData); mParams.finisher.accept(null); mParams.clearImage(); } @VisibleForTesting - List<Notification.Action> populateNotificationActions(Context context, Resources r, Uri uri) { + Notification.Action createShareAction(Context context, Resources r, Uri uri) { // Note: Both the share and edit actions are proxied through ActionProxyReceiver in // order to do some common work like dismissing the keyguard and sending // closeSystemWindows @@ -263,8 +267,6 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // by setting the (otherwise unused) request code to the current user id. int requestCode = context.getUserId(); - ArrayList<Notification.Action> actions = new ArrayList<>(); - PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode, new Intent(context, GlobalScreenshot.TargetChosenReceiver.class), PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); @@ -288,7 +290,15 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder( Icon.createWithResource(r, R.drawable.ic_screenshot_share), r.getString(com.android.internal.R.string.share), shareAction); - actions.add(shareActionBuilder.build()); + + return shareActionBuilder.build(); + } + + @VisibleForTesting + Notification.Action createEditAction(Context context, Resources r, Uri uri) { + // Note: Both the share and edit actions are proxied through ActionProxyReceiver in + // order to do some common work like dismissing the keyguard and sending + // closeSystemWindows // Create an edit intent, if a specific package is provided as the editor, then // launch that directly @@ -302,6 +312,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + // Make sure pending intents for the system user are still unique across users + // by setting the (otherwise unused) request code to the current user id. + int requestCode = mContext.getUserId(); + // Create a edit action PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode, new Intent(context, GlobalScreenshot.ActionProxyReceiver.class) @@ -317,24 +331,30 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { Notification.Action.Builder editActionBuilder = new Notification.Action.Builder( Icon.createWithResource(r, R.drawable.ic_screenshot_edit), r.getString(com.android.internal.R.string.screenshot_edit), editAction); - actions.add(editActionBuilder.build()); - - if (mCreateDeleteAction) { - // Create a delete action for the notification - PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode, - new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class) - .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()) - .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) - .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, - mSmartActionsEnabled) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); - Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder( - Icon.createWithResource(r, R.drawable.ic_screenshot_delete), - r.getString(com.android.internal.R.string.delete), deleteAction); - actions.add(deleteActionBuilder.build()); - } - return actions; + + return editActionBuilder.build(); + } + + @VisibleForTesting + Notification.Action createDeleteAction(Context context, Resources r, Uri uri) { + // Make sure pending intents for the system user are still unique across users + // by setting the (otherwise unused) request code to the current user id. + int requestCode = mContext.getUserId(); + + // Create a delete action for the notification + PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode, + new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class) + .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()) + .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) + .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, + mSmartActionsEnabled) + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND), + PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); + Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder( + Icon.createWithResource(r, R.drawable.ic_screenshot_delete), + r.getString(com.android.internal.R.string.delete), deleteAction); + + return deleteActionBuilder.build(); } private int getUserHandleOfForegroundApplication(Context context) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java new file mode 100644 index 000000000000..20fa991dcc1f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_GLOBAL_ACTIONS; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OVERVIEW; + +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +public enum ScreenshotEvent implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "screenshot requested from global actions") + SCREENSHOT_REQUESTED_GLOBAL_ACTIONS(302), + @UiEvent(doc = "screenshot requested from key chord") + SCREENSHOT_REQUESTED_KEY_CHORD(303), + @UiEvent(doc = "screenshot requested from other key press (e.g. ctrl-s)") + SCREENSHOT_REQUESTED_KEY_OTHER(384), + @UiEvent(doc = "screenshot requested from overview") + SCREENSHOT_REQUESTED_OVERVIEW(304), + @UiEvent(doc = "screenshot requested from accessibility actions") + SCREENSHOT_REQUESTED_ACCESSIBILITY_ACTIONS(382), + @UiEvent(doc = "screenshot requested (other)") + SCREENSHOT_REQUESTED_OTHER(305), + @UiEvent(doc = "screenshot was saved") + SCREENSHOT_SAVED(306), + @UiEvent(doc = "screenshot failed to save") + SCREENSHOT_NOT_SAVED(336), + @UiEvent(doc = "screenshot preview tapped") + SCREENSHOT_PREVIEW_TAPPED(307), + @UiEvent(doc = "screenshot edit button tapped") + SCREENSHOT_EDIT_TAPPED(308), + @UiEvent(doc = "screenshot share button tapped") + SCREENSHOT_SHARE_TAPPED(309), + @UiEvent(doc = "screenshot smart action chip tapped") + SCREENSHOT_SMART_ACTION_TAPPED(374), + @UiEvent(doc = "screenshot scroll tapped") + SCREENSHOT_SCROLL_TAPPED(373), + @UiEvent(doc = "screenshot interaction timed out") + SCREENSHOT_INTERACTION_TIMEOUT(310), + @UiEvent(doc = "screenshot explicitly dismissed") + SCREENSHOT_EXPLICIT_DISMISSAL(311); + + private final int mId; + + ScreenshotEvent(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + + static ScreenshotEvent getScreenshotSource(int source) { + switch (source) { + case SCREENSHOT_GLOBAL_ACTIONS: + return ScreenshotEvent.SCREENSHOT_REQUESTED_GLOBAL_ACTIONS; + case SCREENSHOT_KEY_CHORD: + return ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_CHORD; + case SCREENSHOT_KEY_OTHER: + return ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER; + case SCREENSHOT_OVERVIEW: + return ScreenshotEvent.SCREENSHOT_REQUESTED_OVERVIEW; + case SCREENSHOT_ACCESSIBILITY_ACTIONS: + return ScreenshotEvent.SCREENSHOT_REQUESTED_ACCESSIBILITY_ACTIONS; + case SCREENSHOT_OTHER: + default: + return ScreenshotEvent.SCREENSHOT_REQUESTED_OTHER; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java index 811a8d936b77..fbcd6ba0ff47 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.java @@ -32,7 +32,6 @@ import android.graphics.ColorMatrixColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Picture; -import android.net.Uri; import android.os.UserHandle; import android.util.DisplayMetrics; import android.view.WindowManager; @@ -42,8 +41,6 @@ import com.android.systemui.R; import com.android.systemui.SystemUI; import com.android.systemui.util.NotificationChannels; -import java.util.List; - import javax.inject.Inject; /** @@ -185,23 +182,20 @@ public class ScreenshotNotificationsController { /** * Shows a notification with the saved screenshot and actions that can be taken with it. * - * @param imageUri URI for the saved image - * @param actions a list of notification actions which can be taken + * @param actionData SavedImageData struct with image URI and actions */ public void showScreenshotActionsNotification( - Uri imageUri, - List<Notification.Action> smartActions, - List<Notification.Action> actions) { - for (Notification.Action action : actions) { - mNotificationBuilder.addAction(action); - } - for (Notification.Action smartAction : smartActions) { + GlobalScreenshot.SavedImageData actionData) { + mNotificationBuilder.addAction(actionData.shareAction); + mNotificationBuilder.addAction(actionData.editAction); + mNotificationBuilder.addAction(actionData.deleteAction); + for (Notification.Action smartAction : actionData.smartActions) { mNotificationBuilder.addAction(smartAction); } // Create the intent to show the screenshot in gallery Intent launchIntent = new Intent(Intent.ACTION_VIEW); - launchIntent.setDataAndType(imageUri, "image/png"); + launchIntent.setDataAndType(actionData.uri, "image/png"); launchIntent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java new file mode 100644 index 000000000000..5ced40cb1b3b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot; + +import android.os.IBinder; +import android.view.IWindowManager; + +import javax.inject.Inject; + +/** + * Stub + */ +public class ScrollCaptureController { + + public static final int STATUS_A = 0; + public static final int STATUS_B = 1; + + private final IWindowManager mWindowManagerService; + private StatusListener mListener; + + /** + * + * @param windowManagerService + */ + @Inject + public ScrollCaptureController(IWindowManager windowManagerService) { + mWindowManagerService = windowManagerService; + } + + interface StatusListener { + void onScrollCaptureStatus(boolean available); + } + + /** + * + * @param window + * @param listener + */ + public void getStatus(IBinder window, StatusListener listener) { + mListener = listener; +// try { +// mWindowManagerService.requestScrollCapture(window, new ClientCallbacks()); +// } catch (RemoteException e) { +// } + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 8b8b6f8071e1..a6e083b04ea3 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -32,6 +32,9 @@ import android.os.UserManager; import android.util.Log; import android.view.WindowManager; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.util.ScreenshotHelper; + import java.util.function.Consumer; import javax.inject.Inject; @@ -42,6 +45,7 @@ public class TakeScreenshotService extends Service { private final GlobalScreenshot mScreenshot; private final GlobalScreenshotLegacy mScreenshotLegacy; private final UserManager mUserManager; + private final UiEventLogger mUiEventLogger; private Handler mHandler = new Handler(Looper.myLooper()) { @Override @@ -64,14 +68,22 @@ public class TakeScreenshotService extends Service { return; } - // TODO (mkephart): clean up once notifications flow is fully deprecated + // TODO: clean up once notifications flow is fully deprecated boolean useCornerFlow = true; + + ScreenshotHelper.ScreenshotRequest screenshotRequest = + (ScreenshotHelper.ScreenshotRequest) msg.obj; + + mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshotRequest.getSource())); + switch (msg.what) { case WindowManager.TAKE_SCREENSHOT_FULLSCREEN: if (useCornerFlow) { mScreenshot.takeScreenshot(finisher); } else { - mScreenshotLegacy.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0); + mScreenshotLegacy.takeScreenshot( + finisher, screenshotRequest.getHasStatusBar(), + screenshotRequest.getHasNavBar()); } break; case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION: @@ -79,17 +91,15 @@ public class TakeScreenshotService extends Service { mScreenshot.takeScreenshotPartial(finisher); } else { mScreenshotLegacy.takeScreenshotPartial( - finisher, msg.arg1 > 0, msg.arg2 > 0); + finisher, screenshotRequest.getHasStatusBar(), + screenshotRequest.getHasNavBar()); } break; case WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE: - Bitmap screenshot = msg.getData().getParcelable( - WindowManager.PARCEL_KEY_SCREENSHOT_BITMAP); - Rect screenBounds = msg.getData().getParcelable( - WindowManager.PARCEL_KEY_SCREENSHOT_BOUNDS); - Insets insets = msg.getData().getParcelable( - WindowManager.PARCEL_KEY_SCREENSHOT_INSETS); - int taskId = msg.getData().getInt(WindowManager.PARCEL_KEY_SCREENSHOT_TASK_ID); + Bitmap screenshot = screenshotRequest.getBitmap(); + Rect screenBounds = screenshotRequest.getBoundsInScreen(); + Insets insets = screenshotRequest.getInsets(); + int taskId = screenshotRequest.getTaskId(); if (useCornerFlow) { mScreenshot.handleImageAsScreenshot( screenshot, screenBounds, insets, taskId, finisher); @@ -106,10 +116,12 @@ public class TakeScreenshotService extends Service { @Inject public TakeScreenshotService(GlobalScreenshot globalScreenshot, - GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager) { + GlobalScreenshotLegacy globalScreenshotLegacy, UserManager userManager, + UiEventLogger uiEventLogger) { mScreenshot = globalScreenshot; mScreenshotLegacy = globalScreenshotLegacy; mUserManager = userManager; + mUiEventLogger = uiEventLogger; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt index fa1b0267fafa..4de978c77128 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt @@ -18,8 +18,10 @@ package com.android.systemui.settings import android.content.Context import android.os.UserHandle +import androidx.annotation.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.util.Assert +import java.lang.IllegalStateException import javax.inject.Inject import javax.inject.Singleton @@ -32,7 +34,16 @@ class CurrentUserContextTracker @Inject constructor( broadcastDispatcher: BroadcastDispatcher ) { private val userTracker: CurrentUserTracker - var currentUserContext: Context + private var initialized = false + + private var _curUserContext: Context? = null + val currentUserContext: Context + get() { + if (!initialized) { + throw IllegalStateException("Must initialize before getting context") + } + return _curUserContext!! + } init { userTracker = object : CurrentUserTracker(broadcastDispatcher) { @@ -40,21 +51,21 @@ class CurrentUserContextTracker @Inject constructor( handleUserSwitched(newUserId) } } - - currentUserContext = makeUserContext(userTracker.currentUserId) } fun initialize() { + initialized = true + _curUserContext = makeUserContext(userTracker.currentUserId) userTracker.startTracking() } - private fun handleUserSwitched(newUserId: Int) { - currentUserContext = makeUserContext(newUserId) + @VisibleForTesting + fun handleUserSwitched(newUserId: Int) { + _curUserContext = makeUserContext(newUserId) } private fun makeUserContext(uid: Int): Context { Assert.isMainThread() - return sysuiContext.createContextAsUser( - UserHandle.getUserHandleForUid(userTracker.currentUserId), 0) + return sysuiContext.createContextAsUser(UserHandle.of(uid), 0) } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java index 85dcbb6316d0..5aa7946bcb7f 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/WindowManagerProxy.java @@ -180,12 +180,17 @@ public class WindowManagerProxy { if (isHomeOrRecentTask(rootTask)) { tiles.mHomeAndRecentsSurfaces.add(rootTask.token.getLeash()); } + // Only move resizeable task to split secondary. WM will just ignore this anyways... + if (!rootTask.isResizable()) continue; + // Only move fullscreen tasks to split secondary. if (rootTask.configuration.windowConfiguration.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { continue; } wct.reparent(rootTask.token, tiles.mSecondary.token, true /* onTop */); } + // Move the secondary split-forward. + wct.reorder(tiles.mSecondary.token, true /* onTop */); boolean isHomeResizable = applyHomeTasksMinimized(layout, null /* parent */, wct); WindowOrganizer.applyTransaction(wct); return isHomeResizable; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java index 1b7524521d76..8c24c540e743 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.phone.BarTransitions.TransitionMode; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.statusbar.phone.NavigationBarFragment; import com.android.systemui.statusbar.phone.NavigationBarView; +import com.android.systemui.statusbar.phone.NavigationModeController; import com.android.systemui.statusbar.policy.BatteryController; import javax.inject.Inject; @@ -139,7 +140,8 @@ public class NavigationBarController implements Callbacks { ? Dependency.get(LightBarController.class) : new LightBarController(context, Dependency.get(DarkIconDispatcher.class), - Dependency.get(BatteryController.class)); + Dependency.get(BatteryController.class), + Dependency.get(NavigationModeController.class)); navBar.setLightBarController(lightBarController); // TODO(b/118592525): to support multi-display, we start to add something which is diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 37fc13e2df5b..afb50027f03d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -123,7 +123,6 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications); mBubbleController = bubbleController; mDynamicPrivacyController = privacyController; - privacyController.addListener(this); mDynamicChildBindController = dynamicChildBindController; } @@ -131,6 +130,7 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle NotificationListContainer listContainer) { mPresenter = presenter; mListContainer = listContainer; + mDynamicPrivacyController.addListener(this); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index 4e6df0ad1ba4..d364689a65d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -23,11 +23,14 @@ import android.view.View; import com.android.systemui.DejankUtils; import com.android.systemui.bubbles.BubbleController; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.phone.StatusBar; import java.util.Optional; +import javax.inject.Inject; + /** * Click handler for generic clicks on notifications. Clicks on specific areas (expansion caret, * app ops icon, etc) are handled elsewhere. @@ -35,15 +38,19 @@ import java.util.Optional; public final class NotificationClicker implements View.OnClickListener { private static final String TAG = "NotificationClicker"; - private final Optional<StatusBar> mStatusBar; private final BubbleController mBubbleController; + private final NotificationClickerLogger mLogger; + private final Optional<StatusBar> mStatusBar; private final NotificationActivityStarter mNotificationActivityStarter; - public NotificationClicker(Optional<StatusBar> statusBar, + private NotificationClicker( BubbleController bubbleController, + NotificationClickerLogger logger, + Optional<StatusBar> statusBar, NotificationActivityStarter notificationActivityStarter) { - mStatusBar = statusBar; mBubbleController = bubbleController; + mLogger = logger; + mStatusBar = statusBar; mNotificationActivityStarter = notificationActivityStarter; } @@ -58,25 +65,26 @@ public final class NotificationClicker implements View.OnClickListener { SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK")); final ExpandableNotificationRow row = (ExpandableNotificationRow) v; - final StatusBarNotification sbn = row.getEntry().getSbn(); - if (sbn == null) { - Log.e(TAG, "NotificationClicker called on an unclickable notification,"); - return; - } + final NotificationEntry entry = row.getEntry(); + mLogger.logOnClick(entry); // Check if the notification is displaying the menu, if so slide notification back if (isMenuVisible(row)) { + mLogger.logMenuVisible(entry); row.animateTranslateNotification(0); return; } else if (row.isChildInGroup() && isMenuVisible(row.getNotificationParent())) { + mLogger.logParentMenuVisible(entry); row.getNotificationParent().animateTranslateNotification(0); return; } else if (row.isSummaryWithChildren() && row.areChildrenExpanded()) { // We never want to open the app directly if the user clicks in between // the notifications. + mLogger.logChildrenExpanded(entry); return; } else if (row.areGutsExposed()) { // ignore click if guts are exposed + mLogger.logGutsExposed(entry); return; } @@ -88,7 +96,7 @@ public final class NotificationClicker implements View.OnClickListener { mBubbleController.collapseStack(); } - mNotificationActivityStarter.onNotificationClicked(sbn, row); + mNotificationActivityStarter.onNotificationClicked(entry.getSbn(), row); } private boolean isMenuVisible(ExpandableNotificationRow row) { @@ -107,4 +115,30 @@ public final class NotificationClicker implements View.OnClickListener { row.setOnClickListener(null); } } + + /** Daggerized builder for NotificationClicker. */ + public static class Builder { + private final BubbleController mBubbleController; + private final NotificationClickerLogger mLogger; + + @Inject + public Builder( + BubbleController bubbleController, + NotificationClickerLogger logger) { + mBubbleController = bubbleController; + mLogger = logger; + } + + /** Builds an instance. */ + public NotificationClicker build( + Optional<StatusBar> statusBar, + NotificationActivityStarter notificationActivityStarter + ) { + return new NotificationClicker( + mBubbleController, + mLogger, + statusBar, + notificationActivityStarter); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt new file mode 100644 index 000000000000..fbf033bd2291 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClickerLogger.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.dagger.NotifInteractionLog +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import javax.inject.Inject + +class NotificationClickerLogger @Inject constructor( + @NotifInteractionLog private val buffer: LogBuffer +) { + fun logOnClick(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + str2 = entry.ranking.channel.id + }, { + "CLICK $str1 (channel=$str2)" + }) + } + + fun logMenuVisible(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + }, { + "Ignoring click on $str1; menu is visible" + }) + } + + fun logParentMenuVisible(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + }, { + "Ignoring click on $str1; parent menu is visible" + }) + } + + fun logChildrenExpanded(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + }, { + "Ignoring click on $str1; children are expanded" + }) + } + + fun logGutsExposed(entry: NotificationEntry) { + buffer.log(TAG, LogLevel.DEBUG, { + str1 = entry.key + }, { + "Ignoring click on $str1; guts are exposed" + }) + } +} + +private const val TAG = "NotificationClicker" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 5236385b3716..cb0c2838c24d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -378,6 +378,7 @@ public final class NotificationEntry extends ListEntry { /** * Returns the data needed for a bubble for this notification, if it exists. */ + @Nullable public Notification.BubbleMetadata getBubbleMetadata() { return mBubbleMetadata; } @@ -385,7 +386,7 @@ public final class NotificationEntry extends ListEntry { /** * Sets bubble metadata for this notification. */ - public void setBubbleMetadata(Notification.BubbleMetadata metadata) { + public void setBubbleMetadata(@Nullable Notification.BubbleMetadata metadata) { mBubbleMetadata = metadata; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 7f2f898565d8..c9754048e1d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification -import com.android.systemui.bubbles.BubbleController import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.FeatureFlags import com.android.systemui.statusbar.NotificationListener @@ -62,12 +61,12 @@ class NotificationsControllerImpl @Inject constructor( private val deviceProvisionedController: DeviceProvisionedController, private val notificationRowBinder: NotificationRowBinderImpl, private val remoteInputUriController: RemoteInputUriController, - private val bubbleController: BubbleController, private val groupManager: NotificationGroupManager, private val groupAlertTransferHelper: NotificationGroupAlertTransferHelper, private val headsUpManager: HeadsUpManager, private val headsUpController: HeadsUpController, - private val headsUpViewBinder: HeadsUpViewBinder + private val headsUpViewBinder: HeadsUpViewBinder, + private val clickerBuilder: NotificationClicker.Builder ) : NotificationsController { override fun initialize( @@ -87,10 +86,7 @@ class NotificationsControllerImpl @Inject constructor( listController.bind() notificationRowBinder.setNotificationClicker( - NotificationClicker( - Optional.of(statusBar), - bubbleController, - notificationActivityStarter)) + clickerBuilder.build(Optional.of(statusBar), notificationActivityStarter)) notificationRowBinder.setUpWithPresenter( presenter, listContainer, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java index 6d14ccf85716..9b6ae9a7f99d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/HeadsUpController.java @@ -175,7 +175,7 @@ public class HeadsUpController { private OnHeadsUpChangedListener mOnHeadsUpChangedListener = new OnHeadsUpChangedListener() { @Override public void onHeadsUpStateChanged(@NonNull NotificationEntry entry, boolean isHeadsUp) { - if (!isHeadsUp) { + if (!isHeadsUp && !entry.getRow().isRemoved()) { mHeadsUpViewBinder.unbindHeadsUpView(entry); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index 55a593541819..bcc81a8b967f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -31,6 +31,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; @@ -98,7 +99,7 @@ public class NotificationConversationInfo extends LinearLayout implements private ShortcutInfo mShortcutInfo; private String mConversationId; private StatusBarNotification mSbn; - private Notification.BubbleMetadata mBubbleMetadata; + @Nullable private Notification.BubbleMetadata mBubbleMetadata; private Context mUserContext; private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider; private boolean mIsDeviceProvisioned; @@ -203,6 +204,7 @@ public class NotificationConversationInfo extends LinearLayout implements String pkg, NotificationChannel notificationChannel, NotificationEntry entry, + Notification.BubbleMetadata bubbleMetadata, OnSettingsClickListener onSettingsClick, OnSnoozeClickListener onSnoozeClickListener, ConversationIconFactory conversationIconFactory, @@ -224,7 +226,7 @@ public class NotificationConversationInfo extends LinearLayout implements mOnSnoozeClickListener = onSnoozeClickListener; mIconFactory = conversationIconFactory; mUserContext = userContext; - mBubbleMetadata = entry.getBubbleMetadata(); + mBubbleMetadata = bubbleMetadata; mBuilderProvider = builderProvider; mShortcutManager = shortcutManager; @@ -538,7 +540,8 @@ public class NotificationConversationInfo extends LinearLayout implements Log.e(TAG, "Could not check conversation senders", e); } - boolean showAsBubble = mBubbleMetadata.getAutoExpandBubble() + boolean showAsBubble = mBubbleMetadata != null + && mBubbleMetadata.getAutoExpandBubble() && Settings.Global.getInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 0) == 1; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java index 624fabc0a496..1c808cf90321 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java @@ -366,7 +366,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx final ExpandableNotificationRow row, NotificationConversationInfo notificationInfoView) throws Exception { NotificationGuts guts = row.getGuts(); - StatusBarNotification sbn = row.getEntry().getSbn(); + NotificationEntry entry = row.getEntry(); + StatusBarNotification sbn = entry.getSbn(); String packageName = sbn.getPackageName(); // Settings link is only valid for notifications that specify a non-system user NotificationConversationInfo.OnSettingsClickListener onSettingsClick = null; @@ -407,8 +408,9 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx mNotificationManager, mVisualStabilityManager, packageName, - row.getEntry().getChannel(), - row.getEntry(), + entry.getChannel(), + entry, + entry.getBubbleMetadata(), onSettingsClick, onSnoozeClickListener, iconFactoryLoader, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java index 2da2724aacb2..b96cff830f31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.app.Notification; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaSession; @@ -187,8 +188,10 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi com.android.systemui.R.id.quick_qs_panel); StatusBarNotification sbn = mRow.getEntry().getSbn(); Notification notif = sbn.getNotification(); + Drawable iconDrawable = notif.getSmallIcon().loadDrawable(mContext); panel.getMediaPlayer().setMediaSession(token, - notif.getSmallIcon(), + iconDrawable, + notif.getLargeIcon(), tintColor, mBackgroundColor, mActions, @@ -198,7 +201,8 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi QSPanel bigPanel = ctrl.getNotificationShadeView().findViewById( com.android.systemui.R.id.quick_settings_panel); bigPanel.addMediaSession(token, - notif.getSmallIcon(), + iconDrawable, + notif.getLargeIcon(), tintColor, mBackgroundColor, mActions, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java index 7d422e3c15a2..f9119c7a010f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java @@ -53,11 +53,13 @@ import android.view.WindowManagerGlobal; import com.android.internal.policy.GestureNavigationSettingsObserver; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.bubbles.BubbleController; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.NavigationEdgeBackPlugin; import com.android.systemui.plugins.PluginListener; import com.android.systemui.recents.OverviewProxyService; +import com.android.systemui.settings.CurrentUserTracker; import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; @@ -74,7 +76,7 @@ import java.util.concurrent.Executor; /** * Utility class to handle edge swipes for back gesture */ -public class EdgeBackGestureHandler implements DisplayListener, +public class EdgeBackGestureHandler extends CurrentUserTracker implements DisplayListener, PluginListener<NavigationEdgeBackPlugin>, ProtoTraceable<SystemUiTraceProto> { private static final String TAG = "EdgeBackGestureHandler"; @@ -165,6 +167,7 @@ public class EdgeBackGestureHandler implements DisplayListener, private boolean mIsGesturalModeEnabled; private boolean mIsEnabled; private boolean mIsNavBarShownTransiently; + private boolean mIsBackGestureAllowed; private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; @@ -200,7 +203,7 @@ public class EdgeBackGestureHandler implements DisplayListener, public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService, SysUiState sysUiFlagContainer, PluginManager pluginManager) { - final Resources res = context.getResources(); + super(Dependency.get(BroadcastDispatcher.class)); mContext = context; mDisplayId = context.getDisplayId(); mMainExecutor = context.getMainExecutor(); @@ -216,20 +219,30 @@ public class EdgeBackGestureHandler implements DisplayListener, ViewConfiguration.getLongPressTimeout()); mGestureNavigationSettingsObserver = new GestureNavigationSettingsObserver( - mContext.getMainThreadHandler(), mContext, () -> updateCurrentUserResources(res)); + mContext.getMainThreadHandler(), mContext, this::updateCurrentUserResources); - updateCurrentUserResources(res); + updateCurrentUserResources(); sysUiFlagContainer.addCallback(sysUiFlags -> mSysUiFlags = sysUiFlags); } - public void updateCurrentUserResources(Resources res) { + public void updateCurrentUserResources() { + Resources res = Dependency.get(NavigationModeController.class).getCurrentUserContext() + .getResources(); mEdgeWidthLeft = mGestureNavigationSettingsObserver.getLeftSensitivity(res); mEdgeWidthRight = mGestureNavigationSettingsObserver.getRightSensitivity(res); + mIsBackGestureAllowed = + !mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible(); mBottomGestureHeight = res.getDimensionPixelSize( com.android.internal.R.dimen.navigation_bar_gesture_height); } + @Override + public void onUserSwitched(int newUserId) { + updateIsEnabled(); + updateCurrentUserResources(); + } + /** * @see NavigationBarView#onAttachedToWindow() */ @@ -243,6 +256,7 @@ public class EdgeBackGestureHandler implements DisplayListener, Settings.Global.getUriFor(FIXED_ROTATION_TRANSFORM_SETTING_NAME), false /* notifyForDescendants */, mFixedRotationObserver, UserHandle.USER_ALL); updateIsEnabled(); + startTracking(); } /** @@ -255,6 +269,7 @@ public class EdgeBackGestureHandler implements DisplayListener, } mContext.getContentResolver().unregisterContentObserver(mFixedRotationObserver); updateIsEnabled(); + stopTracking(); } private void setRotationCallbacks(boolean enable) { @@ -269,10 +284,13 @@ public class EdgeBackGestureHandler implements DisplayListener, } } - public void onNavigationModeChanged(int mode, Context currentUserContext) { + /** + * @see NavigationModeController.ModeChangedListener#onNavigationModeChanged + */ + public void onNavigationModeChanged(int mode) { mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode); updateIsEnabled(); - updateCurrentUserResources(currentUserContext.getResources()); + updateCurrentUserResources(); } public void onNavBarTransientStateChanged(boolean isTransient) { @@ -363,6 +381,10 @@ public class EdgeBackGestureHandler implements DisplayListener, updateDisplaySize(); } + public boolean isHandlingGestures() { + return mIsEnabled && mIsBackGestureAllowed; + } + private WindowManager.LayoutParams createLayoutParams() { Resources resources = mContext.getResources(); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( @@ -469,9 +491,9 @@ public class EdgeBackGestureHandler implements DisplayListener, mIsOnLeftEdge = ev.getX() <= mEdgeWidthLeft + mLeftInset; mLogGesture = false; mInRejectedExclusion = false; - mAllowGesture = !QuickStepContract.isBackGestureDisabled(mSysUiFlags) - && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()) - && !mDisabledForQuickstep; + mAllowGesture = !mDisabledForQuickstep && mIsBackGestureAllowed + && !QuickStepContract.isBackGestureDisabled(mSysUiFlags) + && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()); if (mAllowGesture) { mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge); mEdgeBackPlugin.onMotionEvent(ev); @@ -599,6 +621,7 @@ public class EdgeBackGestureHandler implements DisplayListener, public void dump(PrintWriter pw) { pw.println("EdgeBackGestureHandler:"); pw.println(" mIsEnabled=" + mIsEnabled); + pw.println(" mIsBackGestureAllowed=" + mIsBackGestureAllowed); pw.println(" mAllowGesture=" + mAllowGesture); pw.println(" mDisabledForQuickstep=" + mDisabledForQuickstep); pw.println(" mInRejectedExclusion" + mInRejectedExclusion); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index 90bc075b399d..ae7867d68af4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -85,6 +85,8 @@ import com.android.systemui.statusbar.policy.PreviewInflater; import com.android.systemui.tuner.LockscreenFragment.LockButtonFactory; import com.android.systemui.tuner.TunerService; +import java.util.concurrent.Executor; + /** * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status * text. @@ -553,7 +555,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL } }; if (!mKeyguardStateController.canDismissLockScreen()) { - AsyncTask.execute(runnable); + Dependency.get(Executor.class).execute(runnable); } else { boolean dismissShade = !TextUtils.isEmpty(mRightButtonStr) && Dependency.get(TunerService.class).getValue(LOCKSCREEN_RIGHT_UNLOCK, 1) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java index 82e02b47974c..39949c82661f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java @@ -400,6 +400,9 @@ public class KeyguardBouncer { mExpansionCallback.onFullyHidden(); } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) { mExpansionCallback.onStartingToHide(); + if (mKeyguardView != null) { + mKeyguardView.onStartingToHide(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java index d35e1e1e176a..3e5eb5fba8f2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java @@ -33,6 +33,7 @@ import com.android.internal.view.AppearanceRegion; import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.plugins.DarkIconDispatcher; +import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.policy.BatteryController; import java.io.FileDescriptor; @@ -58,6 +59,7 @@ public class LightBarController implements BatteryController.BatteryStateChangeC private AppearanceRegion[] mAppearanceRegions = new AppearanceRegion[0]; private int mStatusBarMode; private int mNavigationBarMode; + private int mNavigationMode; private final Color mDarkModeColor; /** @@ -84,11 +86,14 @@ public class LightBarController implements BatteryController.BatteryStateChangeC @Inject public LightBarController(Context ctx, DarkIconDispatcher darkIconDispatcher, - BatteryController batteryController) { + BatteryController batteryController, NavigationModeController navModeController) { mDarkModeColor = Color.valueOf(ctx.getColor(R.color.dark_mode_icon_color_single_tone)); mStatusBarIconController = (SysuiDarkIconDispatcher) darkIconDispatcher; mBatteryController = batteryController; mBatteryController.addCallback(this); + mNavigationMode = navModeController.addListener((mode) -> { + mNavigationMode = mode; + }); } public void setNavigationBar(LightBarTransitionsController navigationBar) { @@ -234,7 +239,8 @@ public class LightBarController implements BatteryController.BatteryStateChangeC } private void updateNavigation() { - if (mNavigationBarController != null) { + if (mNavigationBarController != null + && !QuickStepContract.isGesturalMode(mNavigationMode)) { mNavigationBarController.setIconsDark(mNavigationLight, animateChange()); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 84aecd4e0759..2978772cac5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -123,7 +123,7 @@ public class NavigationBarView extends FrameLayout implements private KeyButtonDrawable mRecentIcon; private KeyButtonDrawable mDockedIcon; - private final EdgeBackGestureHandler mEdgeBackGestureHandler; + private EdgeBackGestureHandler mEdgeBackGestureHandler; private final DeadZone mDeadZone; private boolean mDeadZoneConsuming = false; private final NavigationBarTransitions mBarTransitions; @@ -244,7 +244,7 @@ public class NavigationBarView extends FrameLayout implements private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = info -> { // When the nav bar is in 2-button or 3-button mode, or when IME is visible in fully // gestural mode, the entire nav bar should be touchable. - if (!isGesturalMode(mNavBarMode) || mImeVisible) { + if (!mEdgeBackGestureHandler.isHandlingGestures() || mImeVisible) { info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); return; } @@ -296,8 +296,6 @@ public class NavigationBarView extends FrameLayout implements R.style.RotateButtonCCWStart90, isGesturalMode ? mFloatingRotationButton : rotateSuggestionButton); - final ContextualButton backButton = new ContextualButton(R.id.back, 0); - mConfiguration = new Configuration(); mTmpLastConfiguration = new Configuration(); mConfiguration.updateFrom(context.getResources().getConfiguration()); @@ -305,7 +303,7 @@ public class NavigationBarView extends FrameLayout implements mScreenPinningNotify = new ScreenPinningNotify(mContext); mBarTransitions = new NavigationBarTransitions(this, Dependency.get(CommandQueue.class)); - mButtonDispatchers.put(R.id.back, backButton); + mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back)); mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home)); mButtonDispatchers.put(R.id.home_handle, new ButtonDispatcher(R.id.home_handle)); mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps)); @@ -659,7 +657,7 @@ public class NavigationBarView extends FrameLayout implements boolean disableHomeHandle = disableRecent && ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0); - boolean disableBack = !useAltBack && (isGesturalMode(mNavBarMode) + boolean disableBack = !useAltBack && (mEdgeBackGestureHandler.isHandlingGestures() || ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0)); // When screen pinning, don't hide back and home when connected service or back and @@ -686,9 +684,9 @@ public class NavigationBarView extends FrameLayout implements } } - getBackButton().setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE); - getHomeButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); - getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); + getBackButton().setVisibility(disableBack ? View.INVISIBLE : View.VISIBLE); + getHomeButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); + getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); getHomeHandle().setVisibility(disableHomeHandle ? View.INVISIBLE : View.VISIBLE); } @@ -838,10 +836,9 @@ public class NavigationBarView extends FrameLayout implements @Override public void onNavigationModeChanged(int mode) { - Context curUserCtx = Dependency.get(NavigationModeController.class).getCurrentUserContext(); mNavBarMode = mode; mBarTransitions.onNavigationModeChanged(mNavBarMode); - mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode, curUserCtx); + mEdgeBackGestureHandler.onNavigationModeChanged(mNavBarMode); mRecentsOnboarding.onNavigationModeChanged(mNavBarMode); getRotateSuggestionButton().onNavigationModeChanged(mNavBarMode); @@ -864,6 +861,7 @@ public class NavigationBarView extends FrameLayout implements @Override public void onFinishInflate() { + super.onFinishInflate(); mNavigationInflaterView = findViewById(R.id.navigation_inflater); mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java index d24ccf343a3a..6061b1e73d1c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationModeController.java @@ -17,9 +17,6 @@ package com.android.systemui.statusbar.phone; import static android.content.Intent.ACTION_OVERLAY_CHANGED; -import static android.content.Intent.ACTION_PREFERRED_ACTIVITY_CHANGED; -import static android.os.UserHandle.USER_CURRENT; -import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY; @@ -38,17 +35,14 @@ import android.os.UserHandle; import android.provider.Settings; import android.provider.Settings.Secure; import android.util.Log; -import android.util.SparseBooleanArray; import com.android.systemui.Dumpable; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.statusbar.policy.DeviceProvisionedController; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Arrays; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -70,104 +64,34 @@ public class NavigationModeController implements Dumpable { private final Context mContext; private Context mCurrentUserContext; private final IOverlayManager mOverlayManager; - private final DeviceProvisionedController mDeviceProvisionedController; private final Executor mUiBgExecutor; - private SparseBooleanArray mRestoreGesturalNavBarMode = new SparseBooleanArray(); - private ArrayList<ModeChangedListener> mListeners = new ArrayList<>(); private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - switch (intent.getAction()) { - case ACTION_OVERLAY_CHANGED: if (DEBUG) { Log.d(TAG, "ACTION_OVERLAY_CHANGED"); } updateCurrentInteractionMode(true /* notify */); - break; - } } }; - private final DeviceProvisionedController.DeviceProvisionedListener mDeviceProvisionedCallback = - new DeviceProvisionedController.DeviceProvisionedListener() { - @Override - public void onDeviceProvisionedChanged() { - if (DEBUG) { - Log.d(TAG, "onDeviceProvisionedChanged: " - + mDeviceProvisionedController.isDeviceProvisioned()); - } - // Once the device has been provisioned, check if we can restore gestural nav - restoreGesturalNavOverlayIfNecessary(); - } - - @Override - public void onUserSetupChanged() { - if (DEBUG) { - Log.d(TAG, "onUserSetupChanged: " - + mDeviceProvisionedController.isCurrentUserSetup()); - } - // Once the user has been setup, check if we can restore gestural nav - restoreGesturalNavOverlayIfNecessary(); - } - - @Override - public void onUserSwitched() { - if (DEBUG) { - Log.d(TAG, "onUserSwitched: " - + ActivityManagerWrapper.getInstance().getCurrentUserId()); - } - - // Update the nav mode for the current user - updateCurrentInteractionMode(true /* notify */); - - // When switching users, defer enabling the gestural nav overlay until the user - // is all set up - deferGesturalNavOverlayIfNecessary(); - } - }; - @Inject - public NavigationModeController(Context context, - DeviceProvisionedController deviceProvisionedController, - @UiBackground Executor uiBgExecutor) { + public NavigationModeController(Context context, @UiBackground Executor uiBgExecutor) { mContext = context; mCurrentUserContext = context; mOverlayManager = IOverlayManager.Stub.asInterface( ServiceManager.getService(Context.OVERLAY_SERVICE)); mUiBgExecutor = uiBgExecutor; - mDeviceProvisionedController = deviceProvisionedController; - mDeviceProvisionedController.addCallback(mDeviceProvisionedCallback); IntentFilter overlayFilter = new IntentFilter(ACTION_OVERLAY_CHANGED); overlayFilter.addDataScheme("package"); overlayFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL); mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, overlayFilter, null, null); - IntentFilter preferredActivityFilter = new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED); - mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, preferredActivityFilter, null, - null); - updateCurrentInteractionMode(false /* notify */); - - // Check if we need to defer enabling gestural nav - deferGesturalNavOverlayIfNecessary(); - } - - private boolean setGestureModeOverlayForMainLauncher() { - if (getCurrentInteractionMode(mCurrentUserContext) == NAV_BAR_MODE_GESTURAL) { - // Already in gesture mode - return true; - } - - Log.d(TAG, "Switching system navigation to full-gesture mode:" - + " contextUser=" - + mCurrentUserContext.getUserId()); - - setModeOverlay(NAV_BAR_MODE_GESTURAL_OVERLAY, USER_CURRENT); - return true; } public void updateCurrentInteractionMode(boolean notify) { @@ -176,10 +100,9 @@ public class NavigationModeController implements Dumpable { if (mode == NAV_BAR_MODE_GESTURAL) { switchToDefaultGestureNavOverlayIfNecessary(); } - mUiBgExecutor.execute(() -> { + mUiBgExecutor.execute(() -> Settings.Secure.putString(mCurrentUserContext.getContentResolver(), - Secure.NAVIGATION_MODE, String.valueOf(mode)); - }); + Secure.NAVIGATION_MODE, String.valueOf(mode))); if (DEBUG) { Log.e(TAG, "updateCurrentInteractionMode: mode=" + mode); dumpAssetPaths(mCurrentUserContext); @@ -230,61 +153,11 @@ public class NavigationModeController implements Dumpable { } } - private void deferGesturalNavOverlayIfNecessary() { - final int userId = mDeviceProvisionedController.getCurrentUser(); - mRestoreGesturalNavBarMode.put(userId, false); - if (mDeviceProvisionedController.isDeviceProvisioned() - && mDeviceProvisionedController.isCurrentUserSetup()) { - // User is already setup and device is provisioned, nothing to do - if (DEBUG) { - Log.d(TAG, "deferGesturalNavOverlayIfNecessary: device is provisioned and user is " - + "setup"); - } - return; - } - - ArrayList<String> defaultOverlays = new ArrayList<>(); - try { - defaultOverlays.addAll(Arrays.asList(mOverlayManager.getDefaultOverlayPackages())); - } catch (RemoteException e) { - Log.e(TAG, "deferGesturalNavOverlayIfNecessary: failed to fetch default overlays"); - } - if (!defaultOverlays.contains(NAV_BAR_MODE_GESTURAL_OVERLAY)) { - // No default gesture nav overlay - if (DEBUG) { - Log.d(TAG, "deferGesturalNavOverlayIfNecessary: no default gestural overlay, " - + "default=" + defaultOverlays); - } - return; - } - - // If the default is gestural, force-enable three button mode until the device is - // provisioned - setModeOverlay(NAV_BAR_MODE_3BUTTON_OVERLAY, USER_CURRENT); - mRestoreGesturalNavBarMode.put(userId, true); - - if (DEBUG) { - Log.d(TAG, "deferGesturalNavOverlayIfNecessary: setting to 3 button mode"); - } - } - - private void restoreGesturalNavOverlayIfNecessary() { - if (DEBUG) { - Log.d(TAG, "restoreGesturalNavOverlayIfNecessary: needs restore=" - + mRestoreGesturalNavBarMode); - } - final int userId = mDeviceProvisionedController.getCurrentUser(); - if (mRestoreGesturalNavBarMode.get(userId)) { - // Restore the gestural state if necessary - setGestureModeOverlayForMainLauncher(); - mRestoreGesturalNavBarMode.put(userId, false); - } - } - private void switchToDefaultGestureNavOverlayIfNecessary() { final int userId = mCurrentUserContext.getUserId(); try { - final IOverlayManager om = mOverlayManager; + final IOverlayManager om = IOverlayManager.Stub.asInterface( + ServiceManager.getService(Context.OVERLAY_SERVICE)); final OverlayInfo info = om.getOverlayInfo(NAV_BAR_MODE_GESTURAL_OVERLAY, userId); if (info != null && !info.isEnabled()) { // Enable the default gesture nav overlay, and move the back gesture inset scale to @@ -309,20 +182,6 @@ public class NavigationModeController implements Dumpable { } } - public void setModeOverlay(String overlayPkg, int userId) { - mUiBgExecutor.execute(() -> { - try { - mOverlayManager.setEnabledExclusiveInCategory(overlayPkg, userId); - if (DEBUG) { - Log.d(TAG, "setModeOverlay: overlayPackage=" + overlayPkg - + " userId=" + userId); - } - } catch (SecurityException | IllegalStateException | RemoteException e) { - Log.e(TAG, "Failed to enable overlay " + overlayPkg + " for user " + userId); - } - }); - } - @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("NavigationModeController:"); @@ -334,11 +193,6 @@ public class NavigationModeController implements Dumpable { defaultOverlays = "failed_to_fetch"; } pw.println(" defaultOverlays=" + defaultOverlays); - pw.println(" restoreGesturalNavMode:"); - for (int i = 0; i < mRestoreGesturalNavBarMode.size(); i++) { - pw.println(" userId=" + mRestoreGesturalNavBarMode.keyAt(i) - + " shouldRestore=" + mRestoreGesturalNavBarMode.valueAt(i)); - } dumpAssetPaths(mCurrentUserContext); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java index 1a6b415f87db..bf52a7ae2bf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RegionSamplingHelper.java @@ -148,11 +148,6 @@ public class RegionSamplingHelper implements View.OnAttachStateChangeListener, updateSamplingRect(); } - private void postUpdateSamplingListener() { - mHandler.removeCallbacks(mUpdateSamplingListener); - mHandler.post(mUpdateSamplingListener); - } - private void updateSamplingListener() { boolean isSamplingEnabled = mSamplingEnabled && !mSamplingRequestBounds.isEmpty() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 53fa2630a9c3..fbe3e9b19248 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -40,7 +40,6 @@ import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.EventLog; -import android.util.Log; import android.view.RemoteAnimationAdapter; import android.view.View; @@ -91,92 +90,119 @@ import dagger.Lazy; */ public class StatusBarNotificationActivityStarter implements NotificationActivityStarter { - private static final String TAG = "NotifActivityStarter"; - protected static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final Context mContext; + + private final CommandQueue mCommandQueue; + private final Handler mMainThreadHandler; + private final Handler mBackgroundHandler; + private final Executor mUiBgExecutor; + private final NotificationEntryManager mEntryManager; + private final NotifPipeline mNotifPipeline; + private final NotifCollection mNotifCollection; + private final HeadsUpManagerPhone mHeadsUpManager; + private final ActivityStarter mActivityStarter; + private final IStatusBarService mBarService; + private final StatusBarStateController mStatusBarStateController; + private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private final KeyguardManager mKeyguardManager; + private final IDreamManager mDreamManager; + private final BubbleController mBubbleController; private final Lazy<AssistManager> mAssistManagerLazy; - private final NotificationGroupManager mGroupManager; - private final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback; private final NotificationRemoteInputManager mRemoteInputManager; + private final NotificationGroupManager mGroupManager; private final NotificationLockscreenUserManager mLockscreenUserManager; private final ShadeController mShadeController; - private final StatusBar mStatusBar; private final KeyguardStateController mKeyguardStateController; - private final ActivityStarter mActivityStarter; - private final NotificationEntryManager mEntryManager; - private final NotifPipeline mNotifPipeline; - private final NotifCollection mNotifCollection; - private final FeatureFlags mFeatureFlags; - private final StatusBarStateController mStatusBarStateController; private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; + private final LockPatternUtils mLockPatternUtils; + private final StatusBarRemoteInputCallback mStatusBarRemoteInputCallback; + private final ActivityIntentHelper mActivityIntentHelper; + + private final FeatureFlags mFeatureFlags; private final MetricsLogger mMetricsLogger; - private final Context mContext; - private final NotificationPanelViewController mNotificationPanel; + private final StatusBarNotificationActivityStarterLogger mLogger; + + private final StatusBar mStatusBar; private final NotificationPresenter mPresenter; - private final LockPatternUtils mLockPatternUtils; - private final HeadsUpManagerPhone mHeadsUpManager; - private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; - private final KeyguardManager mKeyguardManager; + private final NotificationPanelViewController mNotificationPanel; private final ActivityLaunchAnimator mActivityLaunchAnimator; - private final IStatusBarService mBarService; - private final CommandQueue mCommandQueue; - private final IDreamManager mDreamManager; - private final Handler mMainThreadHandler; - private final Handler mBackgroundHandler; - private final ActivityIntentHelper mActivityIntentHelper; - private final BubbleController mBubbleController; - private final Executor mUiBgExecutor; private boolean mIsCollapsingToShowActivityOverLockscreen; - private StatusBarNotificationActivityStarter(Context context, CommandQueue commandQueue, - Lazy<AssistManager> assistManagerLazy, NotificationPanelViewController panel, - NotificationPresenter presenter, NotificationEntryManager entryManager, - HeadsUpManagerPhone headsUpManager, ActivityStarter activityStarter, - ActivityLaunchAnimator activityLaunchAnimator, IStatusBarService statusBarService, + private StatusBarNotificationActivityStarter( + Context context, + CommandQueue commandQueue, + Handler mainThreadHandler, + Handler backgroundHandler, + Executor uiBgExecutor, + NotificationEntryManager entryManager, + NotifPipeline notifPipeline, + NotifCollection notifCollection, + HeadsUpManagerPhone headsUpManager, + ActivityStarter activityStarter, + IStatusBarService statusBarService, StatusBarStateController statusBarStateController, StatusBarKeyguardViewManager statusBarKeyguardViewManager, KeyguardManager keyguardManager, - IDreamManager dreamManager, NotificationRemoteInputManager remoteInputManager, - StatusBarRemoteInputCallback remoteInputCallback, NotificationGroupManager groupManager, + IDreamManager dreamManager, + BubbleController bubbleController, + Lazy<AssistManager> assistManagerLazy, + NotificationRemoteInputManager remoteInputManager, + NotificationGroupManager groupManager, NotificationLockscreenUserManager lockscreenUserManager, - ShadeController shadeController, StatusBar statusBar, + ShadeController shadeController, KeyguardStateController keyguardStateController, NotificationInterruptStateProvider notificationInterruptStateProvider, - MetricsLogger metricsLogger, LockPatternUtils lockPatternUtils, - Handler mainThreadHandler, Handler backgroundHandler, Executor uiBgExecutor, - ActivityIntentHelper activityIntentHelper, BubbleController bubbleController, - FeatureFlags featureFlags, NotifPipeline notifPipeline, - NotifCollection notifCollection) { + LockPatternUtils lockPatternUtils, + StatusBarRemoteInputCallback remoteInputCallback, + ActivityIntentHelper activityIntentHelper, + + FeatureFlags featureFlags, + MetricsLogger metricsLogger, + StatusBarNotificationActivityStarterLogger logger, + + StatusBar statusBar, + NotificationPresenter presenter, + NotificationPanelViewController panel, + ActivityLaunchAnimator activityLaunchAnimator) { mContext = context; - mNotificationPanel = panel; - mPresenter = presenter; + mCommandQueue = commandQueue; + mMainThreadHandler = mainThreadHandler; + mBackgroundHandler = backgroundHandler; + mUiBgExecutor = uiBgExecutor; + mEntryManager = entryManager; + mNotifPipeline = notifPipeline; + mNotifCollection = notifCollection; mHeadsUpManager = headsUpManager; - mActivityLaunchAnimator = activityLaunchAnimator; + mActivityStarter = activityStarter; mBarService = statusBarService; - mCommandQueue = commandQueue; + mStatusBarStateController = statusBarStateController; mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mKeyguardManager = keyguardManager; mDreamManager = dreamManager; + mBubbleController = bubbleController; + mAssistManagerLazy = assistManagerLazy; mRemoteInputManager = remoteInputManager; + mGroupManager = groupManager; mLockscreenUserManager = lockscreenUserManager; mShadeController = shadeController; - // TODO: use KeyguardStateController#isOccluded to remove this dependency - mStatusBar = statusBar; mKeyguardStateController = keyguardStateController; - mActivityStarter = activityStarter; - mEntryManager = entryManager; - mStatusBarStateController = statusBarStateController; mNotificationInterruptStateProvider = notificationInterruptStateProvider; - mMetricsLogger = metricsLogger; - mAssistManagerLazy = assistManagerLazy; - mGroupManager = groupManager; mLockPatternUtils = lockPatternUtils; - mBackgroundHandler = backgroundHandler; - mUiBgExecutor = uiBgExecutor; + mStatusBarRemoteInputCallback = remoteInputCallback; + mActivityIntentHelper = activityIntentHelper; + mFeatureFlags = featureFlags; - mNotifPipeline = notifPipeline; - mNotifCollection = notifCollection; + mMetricsLogger = metricsLogger; + mLogger = logger; + + // TODO: use KeyguardStateController#isOccluded to remove this dependency + mStatusBar = statusBar; + mPresenter = presenter; + mNotificationPanel = panel; + mActivityLaunchAnimator = activityLaunchAnimator; + if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) { mEntryManager.addNotificationEntryListener(new NotificationEntryListener() { @Override @@ -192,11 +218,6 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } }); } - - mStatusBarRemoteInputCallback = remoteInputCallback; - mMainThreadHandler = mainThreadHandler; - mActivityIntentHelper = activityIntentHelper; - mBubbleController = bubbleController; } /** @@ -207,6 +228,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit */ @Override public void onNotificationClicked(StatusBarNotification sbn, ExpandableNotificationRow row) { + mLogger.logStartingActivityFromClick(sbn.getKey()); + RemoteInputController controller = mRemoteInputManager.getController(); if (controller.isRemoteInputActive(row.getEntry()) && !TextUtils.isEmpty(row.getActiveRemoteInputText())) { @@ -225,7 +248,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit // The only valid case is Bubble notifications. Guard against other cases // entering here. if (intent == null && !isBubble) { - Log.e(TAG, "onNotificationClicked called for non-clickable notification!"); + mLogger.logNonClickableNotification(sbn.getKey()); return; } @@ -258,6 +281,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean isActivityIntent, boolean wasOccluded, boolean showOverLockscreen) { + mLogger.logHandleClickAfterKeyguardDismissed(sbn.getKey()); + // TODO: Some of this code may be able to move to NotificationEntryManager. if (mHeadsUpManager != null && mHeadsUpManager.isAlerting(sbn.getKey())) { // Release the HUN notification to the shade. @@ -304,6 +329,8 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit boolean isActivityIntent, boolean wasOccluded, NotificationEntry parentToCancelFinal) { + mLogger.logHandleClickAfterPanelCollapsed(sbn.getKey()); + String notificationKey = sbn.getKey(); try { // The intent we are sending is for the application, which @@ -343,9 +370,11 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit remoteInputText.toString()); } if (isBubble) { + mLogger.logExpandingBubble(notificationKey); expandBubbleStackOnMainThread(notificationKey); } else { - startNotificationIntent(intent, fillInIntent, row, wasOccluded, isActivityIntent); + startNotificationIntent( + intent, fillInIntent, entry, row, wasOccluded, isActivityIntent); } if (isActivityIntent || isBubble) { mAssistManagerLazy.get().hideAssist(); @@ -392,10 +421,16 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } } - private void startNotificationIntent(PendingIntent intent, Intent fillInIntent, - View row, boolean wasOccluded, boolean isActivityIntent) { + private void startNotificationIntent( + PendingIntent intent, + Intent fillInIntent, + NotificationEntry entry, + View row, + boolean wasOccluded, + boolean isActivityIntent) { RemoteAnimationAdapter adapter = mActivityLaunchAnimator.getLaunchAnimation(row, wasOccluded); + mLogger.logStartNotificationIntent(entry.getKey(), intent); try { if (adapter != null) { ActivityTaskManager.getService() @@ -408,7 +443,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } catch (RemoteException | PendingIntent.CanceledException e) { // the stack trace isn't very helpful here. // Just log the exception message. - Log.w(TAG, "Sending contentIntent failed: " + e); + mLogger.logSendingIntentFailed(e); // TODO: Dismiss Keyguard. } } @@ -438,13 +473,9 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private void handleFullScreenIntent(NotificationEntry entry) { if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) { if (shouldSuppressFullScreenIntent(entry)) { - if (DEBUG) { - Log.d(TAG, "No Fullscreen intent: suppressed by DND: " + entry.getKey()); - } + mLogger.logFullScreenIntentSuppressedByDnD(entry.getKey()); } else if (entry.getImportance() < NotificationManager.IMPORTANCE_HIGH) { - if (DEBUG) { - Log.d(TAG, "No Fullscreen intent: not important enough: " + entry.getKey()); - } + mLogger.logFullScreenIntentNotImportantEnough(entry.getKey()); } else { // Stop screensaver if the notification has a fullscreen intent. // (like an incoming phone call) @@ -457,13 +488,13 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit }); // not immersive & a fullscreen alert should be shown - if (DEBUG) { - Log.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); - } + final PendingIntent fullscreenIntent = + entry.getSbn().getNotification().fullScreenIntent; + mLogger.logSendingFullScreenIntent(entry.getKey(), fullscreenIntent); try { EventLog.writeEvent(EventLogTags.SYSUI_FULLSCREEN_NOTIFICATION, entry.getKey()); - entry.getSbn().getNotification().fullScreenIntent.send(); + fullscreenIntent.send(); entry.notifyFullScreenIntentLaunched(); mMetricsLogger.count("note_fullscreen", 1); } catch (PendingIntent.CanceledException e) { @@ -578,9 +609,10 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit public static class Builder { private final Context mContext; private final CommandQueue mCommandQueue; - private final Lazy<AssistManager> mAssistManagerLazy; + private final Handler mMainThreadHandler; + private final Handler mBackgroundHandler; + private final Executor mUiBgExecutor; private final NotificationEntryManager mEntryManager; - private final FeatureFlags mFeatureFlags; private final NotifPipeline mNotifPipeline; private final NotifCollection mNotifCollection; private final HeadsUpManagerPhone mHeadsUpManager; @@ -590,30 +622,37 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final KeyguardManager mKeyguardManager; private final IDreamManager mDreamManager; + private final BubbleController mBubbleController; + private final Lazy<AssistManager> mAssistManagerLazy; private final NotificationRemoteInputManager mRemoteInputManager; - private final StatusBarRemoteInputCallback mRemoteInputCallback; private final NotificationGroupManager mGroupManager; private final NotificationLockscreenUserManager mLockscreenUserManager; + private final ShadeController mShadeController; private final KeyguardStateController mKeyguardStateController; - private final MetricsLogger mMetricsLogger; + private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; private final LockPatternUtils mLockPatternUtils; - private final Handler mMainThreadHandler; - private final Handler mBackgroundHandler; - private final Executor mUiBgExecutor; + private final StatusBarRemoteInputCallback mRemoteInputCallback; private final ActivityIntentHelper mActivityIntentHelper; - private final BubbleController mBubbleController; - private NotificationPanelViewController mNotificationPanelViewController; - private NotificationInterruptStateProvider mNotificationInterruptStateProvider; - private final ShadeController mShadeController; + + private final FeatureFlags mFeatureFlags; + private final MetricsLogger mMetricsLogger; + private final StatusBarNotificationActivityStarterLogger mLogger; + + private StatusBar mStatusBar; private NotificationPresenter mNotificationPresenter; + private NotificationPanelViewController mNotificationPanelViewController; private ActivityLaunchAnimator mActivityLaunchAnimator; - private StatusBar mStatusBar; @Inject - public Builder(Context context, + public Builder( + Context context, CommandQueue commandQueue, - Lazy<AssistManager> assistManagerLazy, + @Main Handler mainThreadHandler, + @Background Handler backgroundHandler, + @UiBackground Executor uiBgExecutor, NotificationEntryManager entryManager, + NotifPipeline notifPipeline, + NotifCollection notifCollection, HeadsUpManagerPhone headsUpManager, ActivityStarter activityStarter, IStatusBarService statusBarService, @@ -621,27 +660,30 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit StatusBarKeyguardViewManager statusBarKeyguardViewManager, KeyguardManager keyguardManager, IDreamManager dreamManager, + BubbleController bubbleController, + Lazy<AssistManager> assistManagerLazy, NotificationRemoteInputManager remoteInputManager, - StatusBarRemoteInputCallback remoteInputCallback, NotificationGroupManager groupManager, NotificationLockscreenUserManager lockscreenUserManager, + ShadeController shadeController, KeyguardStateController keyguardStateController, NotificationInterruptStateProvider notificationInterruptStateProvider, - MetricsLogger metricsLogger, LockPatternUtils lockPatternUtils, - @Main Handler mainThreadHandler, - @Background Handler backgroundHandler, - @UiBackground Executor uiBgExecutor, + StatusBarRemoteInputCallback remoteInputCallback, ActivityIntentHelper activityIntentHelper, - BubbleController bubbleController, - ShadeController shadeController, + FeatureFlags featureFlags, - NotifPipeline notifPipeline, - NotifCollection notifCollection) { + MetricsLogger metricsLogger, + StatusBarNotificationActivityStarterLogger logger) { + mContext = context; mCommandQueue = commandQueue; - mAssistManagerLazy = assistManagerLazy; + mMainThreadHandler = mainThreadHandler; + mBackgroundHandler = backgroundHandler; + mUiBgExecutor = uiBgExecutor; mEntryManager = entryManager; + mNotifPipeline = notifPipeline; + mNotifCollection = notifCollection; mHeadsUpManager = headsUpManager; mActivityStarter = activityStarter; mStatusBarService = statusBarService; @@ -649,23 +691,21 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mKeyguardManager = keyguardManager; mDreamManager = dreamManager; + mBubbleController = bubbleController; + mAssistManagerLazy = assistManagerLazy; mRemoteInputManager = remoteInputManager; - mRemoteInputCallback = remoteInputCallback; mGroupManager = groupManager; mLockscreenUserManager = lockscreenUserManager; + mShadeController = shadeController; mKeyguardStateController = keyguardStateController; mNotificationInterruptStateProvider = notificationInterruptStateProvider; - mMetricsLogger = metricsLogger; mLockPatternUtils = lockPatternUtils; - mMainThreadHandler = mainThreadHandler; - mBackgroundHandler = backgroundHandler; - mUiBgExecutor = uiBgExecutor; + mRemoteInputCallback = remoteInputCallback; mActivityIntentHelper = activityIntentHelper; - mBubbleController = bubbleController; - mShadeController = shadeController; + mFeatureFlags = featureFlags; - mNotifPipeline = notifPipeline; - mNotifCollection = notifCollection; + mMetricsLogger = metricsLogger; + mLogger = logger; } /** Sets the status bar to use as {@link StatusBar}. */ @@ -692,37 +732,42 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } public StatusBarNotificationActivityStarter build() { - return new StatusBarNotificationActivityStarter(mContext, - mCommandQueue, mAssistManagerLazy, - mNotificationPanelViewController, - mNotificationPresenter, + return new StatusBarNotificationActivityStarter( + mContext, + mCommandQueue, + mMainThreadHandler, + mBackgroundHandler, + mUiBgExecutor, mEntryManager, + mNotifPipeline, + mNotifCollection, mHeadsUpManager, mActivityStarter, - mActivityLaunchAnimator, mStatusBarService, mStatusBarStateController, mStatusBarKeyguardViewManager, mKeyguardManager, mDreamManager, + mBubbleController, + mAssistManagerLazy, mRemoteInputManager, - mRemoteInputCallback, mGroupManager, mLockscreenUserManager, mShadeController, - mStatusBar, mKeyguardStateController, mNotificationInterruptStateProvider, - mMetricsLogger, mLockPatternUtils, - mMainThreadHandler, - mBackgroundHandler, - mUiBgExecutor, + mRemoteInputCallback, mActivityIntentHelper, - mBubbleController, + mFeatureFlags, - mNotifPipeline, - mNotifCollection); + mMetricsLogger, + mLogger, + + mStatusBar, + mNotificationPresenter, + mNotificationPanelViewController, + mActivityLaunchAnimator); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt new file mode 100644 index 000000000000..d118747a0365 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLogger.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import android.app.PendingIntent +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel.DEBUG +import com.android.systemui.log.LogLevel.ERROR +import com.android.systemui.log.LogLevel.INFO +import com.android.systemui.log.LogLevel.WARNING +import com.android.systemui.log.dagger.NotifInteractionLog +import javax.inject.Inject + +class StatusBarNotificationActivityStarterLogger @Inject constructor( + @NotifInteractionLog private val buffer: LogBuffer +) { + fun logStartingActivityFromClick(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "(1/4) onNotificationClicked: $str1" + }) + } + + fun logHandleClickAfterKeyguardDismissed(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "(2/4) handleNotificationClickAfterKeyguardDismissed: $str1" + }) + } + + fun logHandleClickAfterPanelCollapsed(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "(3/4) handleNotificationClickAfterPanelCollapsed: $str1" + }) + } + + fun logStartNotificationIntent(key: String, pendingIntent: PendingIntent) { + buffer.log(TAG, INFO, { + str1 = key + str2 = pendingIntent.intent.toString() + }, { + "(4/4) Starting $str2 for notification $str1" + }) + } + + fun logExpandingBubble(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "Expanding bubble for $str1 (rather than firing intent)" + }) + } + + fun logSendingIntentFailed(e: Exception) { + buffer.log(TAG, WARNING, { + str1 = e.toString() + }, { + "Sending contentIntentFailed: $str1" + }) + } + + fun logNonClickableNotification(key: String) { + buffer.log(TAG, ERROR, { + str1 = key + }, { + "onNotificationClicked called for non-clickable notification! $str1" + }) + } + + fun logFullScreenIntentSuppressedByDnD(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "No Fullscreen intent: suppressed by DND: $str1" + }) + } + + fun logFullScreenIntentNotImportantEnough(key: String) { + buffer.log(TAG, DEBUG, { + str1 = key + }, { + "No Fullscreen intent: not important enough: $str1" + }) + } + + fun logSendingFullScreenIntent(key: String, pendingIntent: PendingIntent) { + buffer.log(TAG, INFO, { + str1 = key + str2 = pendingIntent.intent.toString() + }, { + "Notification $str1 has fullScreenIntent; sending fullScreenIntent $str2" + }) + } +} + +private const val TAG = "NotifActivityStarter" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java index 051bd29bc323..a284335c972e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -357,7 +357,18 @@ public class NetworkControllerImpl extends BroadcastReceiver mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mReceiverHandler); mListening = true; + // Initial setup of connectivity. Handled as if we had received a sticky broadcast of + // ConnectivityManager.CONNECTIVITY_ACTION or ConnectivityManager.INET_CONDITION_ACTION. + mReceiverHandler.post(this::updateConnectivity); + + // Initial setup of WifiSignalController. Handled as if we had received a sticky broadcast + // of WifiManager.WIFI_STATE_CHANGED_ACTION or WifiManager.NETWORK_STATE_CHANGED_ACTION + mReceiverHandler.post(mWifiSignalController::fetchInitialState); updateMobileControllers(); + + // Initial setup of emergency information. Handled as if we had received a sticky broadcast + // of TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED. + mReceiverHandler.post(this::recalculateEmergency); } private void unregisterListeners() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java index b258fd47871a..5257ce4c6bd9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java @@ -102,6 +102,20 @@ public class WifiSignalController extends } /** + * Fetches wifi initial state replacing the initial sticky broadcast. + */ + public void fetchInitialState() { + mWifiTracker.fetchInitialState(); + mCurrentState.enabled = mWifiTracker.enabled; + mCurrentState.connected = mWifiTracker.connected; + mCurrentState.ssid = mWifiTracker.ssid; + mCurrentState.rssi = mWifiTracker.rssi; + mCurrentState.level = mWifiTracker.level; + mCurrentState.statusLabel = mWifiTracker.statusLabel; + notifyListenersIfNecessary(); + } + + /** * Extract wifi state directly from broadcasts about changes in wifi state. */ public void handleBroadcast(Intent intent) { diff --git a/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt b/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt index d65b285adb0c..8880df9959c1 100644 --- a/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt +++ b/packages/SystemUI/src/com/android/systemui/util/RelativeTouchListener.kt @@ -115,7 +115,9 @@ abstract class RelativeTouchListener : View.OnTouchListener { performedLongClick = false handler.postDelayed({ - performedLongClick = v.performLongClick() + if (v.isLongClickable) { + performedLongClick = v.performLongClick() + } }, ViewConfiguration.getLongPressTimeout().toLong()) } diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt index 8625d63a3c7e..db08d64acc10 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt @@ -61,14 +61,17 @@ internal val animators = WeakHashMap<Any, PhysicsAnimator<*>>() /** * Default spring configuration to use for animations where stiffness and/or damping ratio - * were not provided. + * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. */ -private val defaultSpring = PhysicsAnimator.SpringConfig( +private val globalDefaultSpring = PhysicsAnimator.SpringConfig( SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) -/** Default fling configuration to use for animations where friction was not provided. */ -private val defaultFling = PhysicsAnimator.FlingConfig( +/** + * Default fling configuration to use for animations where friction was not provided, and a default + * fling config was not set via [PhysicsAnimator.setDefaultFlingConfig]. + */ +private val globalDefaultFling = PhysicsAnimator.FlingConfig( friction = 1f, min = -Float.MAX_VALUE, max = Float.MAX_VALUE) /** Whether to log helpful debug information about animations. */ @@ -111,6 +114,12 @@ class PhysicsAnimator<T> private constructor (val target: T) { /** End actions to run when all animations have completed. */ private val endActions = ArrayList<EndAction>() + /** SpringConfig to use by default for properties whose springs were not provided. */ + private var defaultSpring: SpringConfig = globalDefaultSpring + + /** FlingConfig to use by default for properties whose fling configs were not provided. */ + private var defaultFling: FlingConfig = globalDefaultFling + /** * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add @@ -204,6 +213,19 @@ class PhysicsAnimator<T> private constructor (val target: T) { } /** + * Springs a property to a given value using the provided configuration options, and a start + * velocity of 0f. + * + * @see spring + */ + fun spring( + property: FloatPropertyCompat<in T>, + toPosition: Float + ): PhysicsAnimator<T> { + return spring(property, toPosition, 0f) + } + + /** * Flings a property using the given start velocity, using a [FlingAnimation] configured using * the provided configuration settings. * @@ -392,6 +414,14 @@ class PhysicsAnimator<T> private constructor (val target: T) { return this } + fun setDefaultSpringConfig(defaultSpring: SpringConfig) { + this.defaultSpring = defaultSpring + } + + fun setDefaultFlingConfig(defaultFling: FlingConfig) { + this.defaultFling = defaultFling + } + /** Starts the animations! */ fun start() { startAction() @@ -752,7 +782,7 @@ class PhysicsAnimator<T> private constructor (val target: T) { ) { constructor() : - this(defaultSpring.stiffness, defaultSpring.dampingRatio) + this(globalDefaultSpring.stiffness, globalDefaultSpring.dampingRatio) constructor(stiffness: Float, dampingRatio: Float) : this(stiffness = stiffness, dampingRatio = dampingRatio, startVelocity = 0f) @@ -782,10 +812,10 @@ class PhysicsAnimator<T> private constructor (val target: T) { internal var startVelocity: Float ) { - constructor() : this(defaultFling.friction) + constructor() : this(globalDefaultFling.friction) constructor(friction: Float) : - this(friction, defaultFling.min, defaultFling.max) + this(friction, globalDefaultFling.min, globalDefaultFling.max) constructor(friction: Float, min: Float, max: Float) : this(friction, min, max, startVelocity = 0f) diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java index cc6d607a60cf..8acfbf2b6996 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java @@ -137,6 +137,36 @@ public abstract class ConcurrencyModule { } /** + * Provide a Background-Thread Executor by default. + */ + @Provides + @Singleton + public static RepeatableExecutor provideRepeatableExecutor(@Background DelayableExecutor exec) { + return new RepeatableExecutorImpl(exec); + } + + /** + * Provide a Background-Thread Executor. + */ + @Provides + @Singleton + @Background + public static RepeatableExecutor provideBackgroundRepeatableExecutor( + @Background DelayableExecutor exec) { + return new RepeatableExecutorImpl(exec); + } + + /** + * Provide a Main-Thread Executor. + */ + @Provides + @Singleton + @Main + public static RepeatableExecutor provideMainRepeatableExecutor(@Main DelayableExecutor exec) { + return new RepeatableExecutorImpl(exec); + } + + /** * Provide an Executor specifically for running UI operations on a separate thread. * * Keep submitted runnables short and to the point, just as with any other UI code. diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutor.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutor.java new file mode 100644 index 000000000000..aefdc992e831 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutor.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * A sub-class of {@link Executor} that allows scheduling commands to execute periodically. + */ +public interface RepeatableExecutor extends Executor { + + /** + * Execute supplied Runnable on the Executors thread after initial delay, and subsequently with + * the given delay between the termination of one execution and the commencement of the next. + * + * Each invocation of the supplied Runnable will be scheduled after the previous invocation + * completes. For example, if you schedule the Runnable with a 60 second delay, and the Runnable + * itself takes 1 second, the effective delay will be 61 seconds between each invocation. + * + * See {@link java.util.concurrent.ScheduledExecutorService#scheduleRepeatedly(Runnable, + * long, long)} + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + default Runnable executeRepeatedly(Runnable r, long initialDelayMillis, long delayMillis) { + return executeRepeatedly(r, initialDelayMillis, delayMillis, TimeUnit.MILLISECONDS); + } + + /** + * Execute supplied Runnable on the Executors thread after initial delay, and subsequently with + * the given delay between the termination of one execution and the commencement of the next.. + * + * See {@link java.util.concurrent.ScheduledExecutorService#scheduleRepeatedly(Runnable, + * long, long)} + * + * @return A Runnable that, when run, removes the supplied argument from the Executor queue. + */ + Runnable executeRepeatedly(Runnable r, long initialDelay, long delay, TimeUnit unit); +} diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutorImpl.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutorImpl.java new file mode 100644 index 000000000000..c03e10e5c981 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/RepeatableExecutorImpl.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import java.util.concurrent.TimeUnit; + +/** + * Implementation of {@link RepeatableExecutor} for SystemUI. + */ +class RepeatableExecutorImpl implements RepeatableExecutor { + + private final DelayableExecutor mExecutor; + + RepeatableExecutorImpl(DelayableExecutor executor) { + mExecutor = executor; + } + + @Override + public void execute(Runnable command) { + mExecutor.execute(command); + } + + @Override + public Runnable executeRepeatedly(Runnable r, long initDelay, long delay, TimeUnit unit) { + ExecutionToken token = new ExecutionToken(r, delay, unit); + token.start(initDelay, unit); + return token::cancel; + } + + private class ExecutionToken implements Runnable { + private final Runnable mCommand; + private final long mDelay; + private final TimeUnit mUnit; + private final Object mLock = new Object(); + private Runnable mCancel; + + ExecutionToken(Runnable r, long delay, TimeUnit unit) { + mCommand = r; + mDelay = delay; + mUnit = unit; + } + + @Override + public void run() { + mCommand.run(); + synchronized (mLock) { + if (mCancel != null) { + mCancel = mExecutor.executeDelayed(this, mDelay, mUnit); + } + } + } + + /** Starts execution that will repeat the command until {@link cancel}. */ + public void start(long startDelay, TimeUnit unit) { + synchronized (mLock) { + mCancel = mExecutor.executeDelayed(this, startDelay, unit); + } + } + + /** Cancel repeated execution of command. */ + public void cancel() { + synchronized (mLock) { + if (mCancel != null) { + mCancel.run(); + mCancel = null; + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java index e5da603321cd..899aabb2e9a7 100644 --- a/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java +++ b/packages/SystemUI/src/com/android/systemui/wm/SystemWindows.java @@ -32,6 +32,7 @@ import android.util.SparseArray; import android.view.Display; import android.view.DisplayCutout; import android.view.DragEvent; +import android.view.IScrollCaptureController; import android.view.IWindow; import android.view.IWindowManager; import android.view.IWindowSession; @@ -352,5 +353,14 @@ public class SystemWindows { @Override public void dispatchPointerCaptureChanged(boolean hasCapture) {} + + @Override + public void requestScrollCapture(IScrollCaptureController controller) { + try { + controller.onClientUnavailable(); + } catch (RemoteException ex) { + // ignore + } + } } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java index 25f279b45d04..dd5c8335eefa 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardHostViewTest.java @@ -17,30 +17,50 @@ package com.android.keyguard; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import com.android.internal.widget.LockPatternUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper public class KeyguardHostViewTest extends SysuiTestCase { + @Mock + private KeyguardSecurityContainer mSecurityContainer; + @Mock + private LockPatternUtils mLockPatternUtils; + @Rule + public MockitoRule mMockitoRule = MockitoJUnit.rule(); + private KeyguardHostView mKeyguardHostView; @Before public void setup() { mDependency.injectMockDependency(KeyguardUpdateMonitor.class); - mKeyguardHostView = new KeyguardHostView(getContext()); + mKeyguardHostView = new KeyguardHostView(getContext()) { + @Override + protected void onFinishInflate() { + mSecurityContainer = KeyguardHostViewTest.this.mSecurityContainer; + mLockPatternUtils = KeyguardHostViewTest.this.mLockPatternUtils; + } + }; + mKeyguardHostView.onFinishInflate(); } @Test @@ -50,4 +70,10 @@ public class KeyguardHostViewTest extends SysuiTestCase { null /* cancelAction */); Assert.assertTrue("Action should exist", mKeyguardHostView.hasDismissActions()); } + + @Test + public void testOnStartingToHide() { + mKeyguardHostView.onStartingToHide(); + verify(mSecurityContainer).onStartingToHide(); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index 7403a11fecbf..6c00ecacf97d 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -61,6 +61,7 @@ import android.os.UserManager; import android.telephony.ServiceState; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; @@ -133,20 +134,23 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock - private Executor mBackgroundExecutor; - @Mock private RingerModeTracker mRingerModeTracker; @Mock private LiveData<Integer> mRingerModeLiveData; + @Mock + private TelephonyManager mTelephonyManager; + // Direct executor + private Executor mBackgroundExecutor = Runnable::run; private TestableLooper mTestableLooper; private TestableKeyguardUpdateMonitor mKeyguardUpdateMonitor; + private TestableContext mSpiedContext; @Before public void setup() { MockitoAnnotations.initMocks(this); - TestableContext context = spy(mContext); + mSpiedContext = spy(mContext); when(mPackageManager.hasSystemFeature(anyString())).thenReturn(true); - when(context.getPackageManager()).thenReturn(mPackageManager); + when(mSpiedContext.getPackageManager()).thenReturn(mPackageManager); doAnswer(invocation -> { IBiometricEnabledOnKeyguardCallback callback = invocation.getArgument(0); callback.onChanged(BiometricSourceType.FACE, true /* enabled */, @@ -161,19 +165,20 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mStrongAuthTracker .isUnlockingWithBiometricAllowed(anyBoolean() /* isStrongBiometric */)) .thenReturn(true); - context.addMockSystemService(TrustManager.class, mTrustManager); - context.addMockSystemService(FingerprintManager.class, mFingerprintManager); - context.addMockSystemService(BiometricManager.class, mBiometricManager); - context.addMockSystemService(FaceManager.class, mFaceManager); - context.addMockSystemService(UserManager.class, mUserManager); - context.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager); - context.addMockSystemService(SubscriptionManager.class, mSubscriptionManager); + mSpiedContext.addMockSystemService(TrustManager.class, mTrustManager); + mSpiedContext.addMockSystemService(FingerprintManager.class, mFingerprintManager); + mSpiedContext.addMockSystemService(BiometricManager.class, mBiometricManager); + mSpiedContext.addMockSystemService(FaceManager.class, mFaceManager); + mSpiedContext.addMockSystemService(UserManager.class, mUserManager); + mSpiedContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager); + mSpiedContext.addMockSystemService(SubscriptionManager.class, mSubscriptionManager); + mSpiedContext.addMockSystemService(TelephonyManager.class, mTelephonyManager); when(mRingerModeTracker.getRingerMode()).thenReturn(mRingerModeLiveData); mTestableLooper = TestableLooper.get(this); allowTestableLooperAsMainThread(); - mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(context); + mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mSpiedContext); } @After @@ -192,6 +197,22 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void testSimStateInitialized() { + final int subId = 3; + final int state = TelephonyManager.SIM_STATE_ABSENT; + + when(mTelephonyManager.getActiveModemCount()).thenReturn(1); + when(mTelephonyManager.getSimState(anyInt())).thenReturn(state); + when(mSubscriptionManager.getSubscriptionIds(anyInt())).thenReturn(new int[] { subId }); + + KeyguardUpdateMonitor testKUM = new TestableKeyguardUpdateMonitor(mSpiedContext); + + mTestableLooper.processAllMessages(); + + assertThat(testKUM.getSimState(subId)).isEqualTo(state); + } + + @Test public void testIgnoresSimStateCallback_rebroadcast() { Intent intent = new Intent(TelephonyIntents.ACTION_SIM_STATE_CHANGED); diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java index 9e18e61229c3..3ef693a1ec98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -85,6 +85,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.LockscreenLockIconController; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.NotificationShadeWindowController; +import com.android.systemui.statusbar.phone.NotificationShadeWindowView; import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -183,6 +184,8 @@ public class BubbleControllerTest extends SysuiTestCase { private DumpManager mDumpManager; @Mock private LockscreenLockIconController mLockIconController; + @Mock + private NotificationShadeWindowView mNotificationShadeWindowView; private SuperStatusBarViewFactory mSuperStatusBarViewFactory; private BubbleData mBubbleData; @@ -219,8 +222,7 @@ public class BubbleControllerTest extends SysuiTestCase { mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, mConfigurationController, mKeyguardBypassController, mColorExtractor, mDumpManager); - mNotificationShadeWindowController.setNotificationShadeView( - mSuperStatusBarViewFactory.getNotificationShadeWindowView()); + mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView); mNotificationShadeWindowController.attach(); // Need notifications for bubbles diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java index 49236e0275e0..8e6fc8a24b31 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java @@ -79,6 +79,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.LockscreenLockIconController; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.phone.NotificationShadeWindowController; +import com.android.systemui.statusbar.phone.NotificationShadeWindowView; import com.android.systemui.statusbar.phone.ShadeController; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -134,6 +135,8 @@ public class NewNotifPipelineBubbleControllerTest extends SysuiTestCase { private KeyguardBypassController mKeyguardBypassController; @Mock private FloatingContentCoordinator mFloatingContentCoordinator; + @Mock + private NotificationShadeWindowView mNotificationShadeWindowView; private SysUiState mSysUiState = new SysUiState(); @@ -206,8 +209,7 @@ public class NewNotifPipelineBubbleControllerTest extends SysuiTestCase { mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController, mConfigurationController, mKeyguardBypassController, mColorExtractor, mDumpManager); - mNotificationShadeWindowController.setNotificationShadeView( - mSuperStatusBarViewFactory.getNotificationShadeWindowView()); + mNotificationShadeWindowController.setNotificationShadeView(mNotificationShadeWindowView); mNotificationShadeWindowController.attach(); // Need notifications for bubbles diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt index f6ee46b0303a..e3f25c6b0c72 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt @@ -89,6 +89,7 @@ class ControlsControllerImplTest : SysuiTestCase() { @Captor private lateinit var controlLoadCallbackCaptor: ArgumentCaptor<ControlsBindingController.LoadCallback> + @Captor private lateinit var broadcastReceiverCaptor: ArgumentCaptor<BroadcastReceiver> @Captor @@ -97,6 +98,7 @@ class ControlsControllerImplTest : SysuiTestCase() { private lateinit var delayableExecutor: FakeExecutor private lateinit var controller: ControlsControllerImpl + private lateinit var canceller: DidRunRunnable companion object { fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() @@ -146,6 +148,9 @@ class ControlsControllerImplTest : SysuiTestCase() { } } + canceller = DidRunRunnable() + `when`(bindingController.bindAndLoad(any(), any())).thenReturn(canceller) + controller = ControlsControllerImpl( wrapper, delayableExecutor, @@ -266,7 +271,7 @@ class ControlsControllerImplTest : SysuiTestCase() { assertTrue(favorites.isEmpty()) assertFalse(data.errorOnLoad) - }) + }, Consumer {}) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) @@ -301,7 +306,7 @@ class ControlsControllerImplTest : SysuiTestCase() { assertEquals(1, favorites.size) assertEquals(TEST_CONTROL_ID, favorites[0]) assertFalse(data.errorOnLoad) - }) + }, Consumer {}) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) @@ -332,7 +337,7 @@ class ControlsControllerImplTest : SysuiTestCase() { assertEquals(1, favorites.size) assertEquals(TEST_CONTROL_ID, favorites[0]) assertFalse(data.errorOnLoad) - }) + }, Consumer {}) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) @@ -363,7 +368,7 @@ class ControlsControllerImplTest : SysuiTestCase() { assertEquals(1, favorites.size) assertEquals(TEST_CONTROL_ID, favorites[0]) assertTrue(data.errorOnLoad) - }) + }, Consumer {}) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) @@ -377,22 +382,15 @@ class ControlsControllerImplTest : SysuiTestCase() { @Test fun testCancelLoad() { - val canceller = object : Runnable { - var ran = false - override fun run() { - ran = true - } - } - `when`(bindingController.bindAndLoad(any(), any())).thenReturn(canceller) - var loaded = false + var cancelRunnable: Runnable? = null controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO) delayableExecutor.runAllReady() controller.loadForComponent(TEST_COMPONENT, Consumer { loaded = true - }) + }, Consumer { runnable -> cancelRunnable = runnable }) - controller.cancelLoad() + cancelRunnable?.run() delayableExecutor.runAllReady() assertFalse(loaded) @@ -400,61 +398,47 @@ class ControlsControllerImplTest : SysuiTestCase() { } @Test - fun testCancelLoad_noCancelAfterSuccessfulLoad() { - val canceller = object : Runnable { - var ran = false - override fun run() { - ran = true - } - } - `when`(bindingController.bindAndLoad(any(), any())).thenReturn(canceller) - + fun testCancelLoad_afterSuccessfulLoad() { var loaded = false + var cancelRunnable: Runnable? = null controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO) delayableExecutor.runAllReady() controller.loadForComponent(TEST_COMPONENT, Consumer { loaded = true - }) + }, Consumer { runnable -> cancelRunnable = runnable }) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) controlLoadCallbackCaptor.value.accept(emptyList()) - controller.cancelLoad() + cancelRunnable?.run() delayableExecutor.runAllReady() assertTrue(loaded) - assertFalse(canceller.ran) + assertTrue(canceller.ran) } @Test - fun testCancelLoad_noCancelAfterErrorLoad() { - val canceller = object : Runnable { - var ran = false - override fun run() { - ran = true - } - } - `when`(bindingController.bindAndLoad(any(), any())).thenReturn(canceller) - + fun testCancelLoad_afterErrorLoad() { var loaded = false + var cancelRunnable: Runnable? = null controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO) delayableExecutor.runAllReady() controller.loadForComponent(TEST_COMPONENT, Consumer { loaded = true - }) + }, Consumer { runnable -> cancelRunnable = runnable }) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) controlLoadCallbackCaptor.value.error("") - controller.cancelLoad() + cancelRunnable?.run() delayableExecutor.runAllReady() assertTrue(loaded) - assertFalse(canceller.ran) + assertTrue(canceller.ran) } @Test @@ -465,7 +449,7 @@ class ControlsControllerImplTest : SysuiTestCase() { val newControlInfo = TEST_CONTROL_INFO.copy(controlTitle = TEST_CONTROL_TITLE_2) val control = statelessBuilderFromInfo(newControlInfo).build() - controller.loadForComponent(TEST_COMPONENT, Consumer {}) + controller.loadForComponent(TEST_COMPONENT, Consumer {}, Consumer {}) verify(bindingController).bindAndLoad(eq(TEST_COMPONENT), capture(controlLoadCallbackCaptor)) @@ -963,3 +947,10 @@ class ControlsControllerImplTest : SysuiTestCase() { assertTrue(controller.getFavoritesForStructure(TEST_COMPONENT_2, TEST_STRUCTURE).isEmpty()) } } + +private class DidRunRunnable() : Runnable { + var ran = false + override fun run() { + ran = true + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java index e8e98b49c47f..4ac5912d0690 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -133,4 +133,12 @@ public class ScreenRecordTileTest extends SysuiTestCase { verify(mController, times(1)).stopRecording(); } + + @Test + public void testContentDescriptionHasTileName() { + mTile.refreshState(); + mTestableLooper.processAllMessages(); + + assertTrue(mTile.getState().contentDescription.toString().contains(mTile.getState().label)); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java index 11649ca34709..ffe3cd5bd504 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java @@ -16,6 +16,10 @@ package com.android.systemui.screenshot; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; @@ -40,7 +44,6 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SystemUIFactory; import com.android.systemui.SysuiTestCase; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -82,9 +85,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { CompletableFuture<List<Notification.Action>> smartActionsFuture = ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap, smartActionsProvider, true, false); - Assert.assertNotNull(smartActionsFuture); + assertNotNull(smartActionsFuture); List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS); - Assert.assertEquals(Collections.emptyList(), smartActions); + assertEquals(Collections.emptyList(), smartActions); } // Tests any exception thrown in waiting for smart actions future to complete does @@ -99,7 +102,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { RuntimeException.class); List<Notification.Action> actions = ScreenshotSmartActions.getSmartActions( "", "", smartActionsFuture, timeoutMs, mSmartActionsProvider); - Assert.assertEquals(Collections.emptyList(), actions); + assertEquals(Collections.emptyList(), actions); } // Tests any exception thrown in notifying feedback does not affect regular screenshot flow. @@ -123,9 +126,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { mSmartActionsProvider, true, true); verify(mSmartActionsProvider, never()).getActions(any(), any(), any(), any(), eq(false)); - Assert.assertNotNull(smartActionsFuture); + assertNotNull(smartActionsFuture); List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS); - Assert.assertEquals(Collections.emptyList(), smartActions); + assertEquals(Collections.emptyList(), smartActions); } // Tests for a hardware bitmap, ScreenshotNotificationSmartActionsProvider is invoked once. @@ -152,14 +155,14 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { ScreenshotSmartActions.getSmartActionsFuture("", "", bitmap, actionsProvider, true, true); - Assert.assertNotNull(smartActionsFuture); + assertNotNull(smartActionsFuture); List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS); - Assert.assertEquals(smartActions.size(), 0); + assertEquals(smartActions.size(), 0); } - // Tests for notification action extras. + // Tests for share action extras @Test - public void testNotificationActionExtras() { + public void testShareActionExtras() { if (Looper.myLooper() == null) { Looper.prepare(); } @@ -169,35 +172,70 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); data.finisher = null; data.mActionsReadyListener = null; - data.createDeleteAction = true; SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data); - List<Notification.Action> actions = task.populateNotificationActions( - mContext, mContext.getResources(), + + Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(), Uri.parse("Screenshot_123.png")); - Assert.assertEquals(actions.size(), 3); - boolean isShareFound = false; - boolean isEditFound = false; - boolean isDeleteFound = false; - for (Notification.Action action : actions) { - Intent intent = action.actionIntent.getIntent(); - Assert.assertNotNull(intent); - Bundle bundle = intent.getExtras(); - Assert.assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID)); - Assert.assertTrue( - bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED)); - - if (action.title.equals(GlobalScreenshot.ACTION_TYPE_DELETE)) { - isDeleteFound = intent.getAction() == null; - } else if (action.title.equals(GlobalScreenshot.ACTION_TYPE_EDIT)) { - isEditFound = Intent.ACTION_EDIT.equals(intent.getAction()); - } else if (action.title.equals(GlobalScreenshot.ACTION_TYPE_SHARE)) { - isShareFound = Intent.ACTION_SEND.equals(intent.getAction()); - } + Intent intent = shareAction.actionIntent.getIntent(); + assertNotNull(intent); + Bundle bundle = intent.getExtras(); + assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID)); + assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED)); + assertEquals(GlobalScreenshot.ACTION_TYPE_SHARE, shareAction.title); + assertEquals(Intent.ACTION_SEND, intent.getAction()); + } + + // Tests for edit action extras + @Test + public void testEditActionExtras() { + if (Looper.myLooper() == null) { + Looper.prepare(); } - Assert.assertTrue(isEditFound); - Assert.assertTrue(isDeleteFound); - Assert.assertTrue(isShareFound); + GlobalScreenshot.SaveImageInBackgroundData + data = new GlobalScreenshot.SaveImageInBackgroundData(); + data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + data.finisher = null; + data.mActionsReadyListener = null; + SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data); + + Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(), + Uri.parse("Screenshot_123.png")); + + Intent intent = editAction.actionIntent.getIntent(); + assertNotNull(intent); + Bundle bundle = intent.getExtras(); + assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID)); + assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED)); + assertEquals(GlobalScreenshot.ACTION_TYPE_EDIT, editAction.title); + assertEquals(Intent.ACTION_EDIT, intent.getAction()); + } + + // Tests for share action extras + @Test + public void testDeleteActionExtras() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + GlobalScreenshot.SaveImageInBackgroundData + data = new GlobalScreenshot.SaveImageInBackgroundData(); + data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + data.finisher = null; + data.mActionsReadyListener = null; + SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data); + + Notification.Action deleteAction = task.createDeleteAction(mContext, + mContext.getResources(), + Uri.parse("Screenshot_123.png")); + + Intent intent = deleteAction.actionIntent.getIntent(); + assertNotNull(intent); + Bundle bundle = intent.getExtras(); + assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID)); + assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED)); + assertEquals(deleteAction.title, GlobalScreenshot.ACTION_TYPE_DELETE); + assertNull(intent.getAction()); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt new file mode 100644 index 000000000000..628c06a56abd --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/CurrentUserContextTrackerTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.settings + +import android.content.Context +import android.content.ContextWrapper +import android.os.UserHandle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import junit.framework.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class CurrentUserContextTrackerTest : SysuiTestCase() { + + private lateinit var tracker: CurrentUserContextTracker + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + allowTestableLooperAsMainThread() + + // wrap Context so that tests don't throw for missing package errors + val wrapped = object : ContextWrapper(context) { + override fun createContextAsUser(user: UserHandle, flags: Int): Context { + val mockContext = mock(Context::class.java) + `when`(mockContext.user).thenReturn(user) + `when`(mockContext.userId).thenReturn(user.identifier) + return mockContext + } + } + + tracker = CurrentUserContextTracker(wrapped, broadcastDispatcher) + tracker.initialize() + } + + @Test + fun testContextExistsAfterInit_noCrash() { + tracker.currentUserContext + } + + @Test + fun testUserContextIsCorrectAfterUserSwitch() { + // We always start out with system ui test + assertTrue("Starting userId should be 0", tracker.currentUserContext.userId == 0) + + // WHEN user changes + tracker.handleUserSwitched(1) + + // THEN user context should have the correct userId + assertTrue("User has changed to userId 1, the context should reflect that", + tracker.currentUserContext.userId == 1) + } + + @Suppress("UNUSED_PARAMETER") + @Test(expected = IllegalStateException::class) + fun testContextTrackerThrowsExceptionWhenNotInitialized() { + // GIVEN an uninitialized CurrentUserContextTracker + val userTracker = CurrentUserContextTracker(context, broadcastDispatcher) + + // WHEN client asks for a context + val userContext = userTracker.currentUserContext + + // THEN an exception is thrown + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java index 61388b6d0389..6db868563d3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfoTest.java @@ -48,9 +48,9 @@ import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Person; -import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.LauncherApps; @@ -91,6 +91,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; @@ -147,14 +148,15 @@ public class NotificationConversationInfoTest extends SysuiTestCase { private ShadeController mShadeController; @Mock private ConversationIconFactory mIconFactory; - @Mock - private Context mUserContext; @Mock(answer = Answers.RETURNS_SELF) private PriorityOnboardingDialogController.Builder mBuilder; private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider = () -> mBuilder; + @Mock + private Notification.BubbleMetadata mBubbleMetadata; @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); mTestableLooper = TestableLooper.get(this); mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); @@ -228,6 +230,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { when(mMockINotificationManager.getConversationNotificationChannel(anyString(), anyInt(), anyString(), eq(TEST_CHANNEL), eq(false), eq(CONVERSATION_ID))) .thenReturn(mConversationChannel); + + when(mMockINotificationManager.getConsolidatedNotificationPolicy()) + .thenReturn(mock(NotificationManager.Policy.class)); + + when(mBuilder.build()).thenReturn(mock(PriorityOnboardingDialogController.class)); } @Test @@ -240,10 +247,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon); @@ -261,10 +269,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name); @@ -283,7 +292,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, - null, + mBubbleMetadata, + null, null, null, true); @@ -308,10 +318,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView textView = mNotificationInfo.findViewById(R.id.group_name); @@ -331,10 +342,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView textView = mNotificationInfo.findViewById(R.id.group_name); @@ -353,10 +365,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); @@ -382,10 +395,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, entry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name); @@ -404,13 +418,14 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, (View v, NotificationChannel c, int appUid) -> { assertEquals(mConversationChannel, c); latch.countDown(); }, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -430,10 +445,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); final View settingsButton = mNotificationInfo.findViewById(R.id.info); @@ -451,13 +467,14 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, (View v, NotificationChannel c, int appUid) -> { assertEquals(mNotificationChannel, c); latch.countDown(); }, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, false); final View settingsButton = mNotificationInfo.findViewById(R.id.info); @@ -476,10 +493,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); View view = mNotificationInfo.findViewById(R.id.silence); @@ -501,10 +519,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); View view = mNotificationInfo.findViewById(R.id.default_behavior); @@ -529,10 +548,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); View view = mNotificationInfo.findViewById(R.id.default_behavior); @@ -556,10 +576,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -596,10 +617,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -635,10 +657,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -675,10 +698,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -709,10 +733,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -741,10 +766,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -774,10 +800,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -807,10 +834,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -839,10 +867,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -870,10 +899,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -892,10 +922,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, + mContext, mBuilderProvider, true); @@ -910,13 +941,12 @@ public class NotificationConversationInfoTest extends SysuiTestCase { // GIVEN the priority onboarding screen is present PriorityOnboardingDialogController.Builder b = - new PriorityOnboardingDialogController.Builder(); + mock(PriorityOnboardingDialogController.Builder.class, Answers.RETURNS_SELF); PriorityOnboardingDialogController controller = mock(PriorityOnboardingDialogController.class); when(b.build()).thenReturn(controller); // GIVEN the user is changing conversation settings - when(mBuilderProvider.get()).thenReturn(b); mNotificationInfo.bindNotification( mShortcutManager, mMockPackageManager, @@ -925,11 +955,12 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, - mBuilderProvider, + mContext, + () -> b, true); // WHEN user clicks "priority" @@ -945,12 +976,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase { Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, true); PriorityOnboardingDialogController.Builder b = - new PriorityOnboardingDialogController.Builder(); + mock(PriorityOnboardingDialogController.Builder.class, Answers.RETURNS_SELF); PriorityOnboardingDialogController controller = mock(PriorityOnboardingDialogController.class); when(b.build()).thenReturn(controller); - when(mBuilderProvider.get()).thenReturn(b); mNotificationInfo.bindNotification( mShortcutManager, mMockPackageManager, @@ -959,11 +989,12 @@ public class NotificationConversationInfoTest extends SysuiTestCase { TEST_PACKAGE_NAME, mNotificationChannel, mEntry, + mBubbleMetadata, null, null, mIconFactory, - mUserContext, - mBuilderProvider, + mContext, + () -> b, true); // WHEN user clicks "priority" diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt index 2b091f297184..210744eb30cf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaTest.kt @@ -6,11 +6,18 @@ import android.view.LayoutInflater import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.assist.AssistManager +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.AccessibilityController +import com.android.systemui.statusbar.policy.FlashlightController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.tuner.TunerService import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations +import java.util.concurrent.Executor @SmallTest @RunWith(AndroidTestingRunner::class) @@ -24,6 +31,15 @@ class KeyguardBottomAreaTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) + // Mocked dependencies + mDependency.injectMockDependency(AccessibilityController::class.java) + mDependency.injectMockDependency(ActivityStarter::class.java) + mDependency.injectMockDependency(AssistManager::class.java) + mDependency.injectTestDependency(Executor::class.java, Executor { it.run() }) + mDependency.injectMockDependency(FlashlightController::class.java) + mDependency.injectMockDependency(KeyguardStateController::class.java) + mDependency.injectMockDependency(TunerService::class.java) + mKeyguardBottomArea = LayoutInflater.from(mContext).inflate( R.layout.keyguard_bottom_area, null, false) as KeyguardBottomAreaView mKeyguardBottomArea.setStatusBar(mStatusBar) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java index e052ae2653f0..0a041e4a4dc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBouncerTest.java @@ -26,7 +26,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @@ -37,6 +36,7 @@ import android.graphics.Color; import android.os.Handler; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; @@ -64,6 +64,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -94,9 +95,11 @@ public class KeyguardBouncerTest extends SysuiTestCase { private Handler mHandler; @Mock private KeyguardSecurityModel mKeyguardSecurityModel; + @Mock + private ViewGroup mRootView; @Rule public MockitoRule mRule = MockitoJUnit.rule(); - private ViewGroup mRootView; + private Integer mRootVisibility = View.INVISIBLE; private KeyguardBouncer mBouncer; @Before @@ -105,6 +108,11 @@ public class KeyguardBouncerTest extends SysuiTestCase { mDependency.injectTestDependency(KeyguardUpdateMonitor.class, mKeyguardUpdateMonitor); mDependency.injectTestDependency(KeyguardSecurityModel.class, mKeyguardSecurityModel); mDependency.injectMockDependency(KeyguardStateController.class); + when(mRootView.getVisibility()).thenAnswer((Answer<Integer>) invocation -> mRootVisibility); + doAnswer(invocation -> { + mRootVisibility = invocation.getArgument(0); + return null; + }).when(mRootView).setVisibility(anyInt()); when(mKeyguardSecurityModel.getSecurityMode(anyInt())) .thenReturn(KeyguardSecurityModel.SecurityMode.None); DejankUtils.setImmediate(true); @@ -117,10 +125,8 @@ public class KeyguardBouncerTest extends SysuiTestCase { mKeyguardBypassController, mHandler) { @Override protected void inflateView() { - super.inflateView(); mKeyguardView = mKeyguardHostView; - mRoot = spy(mRoot); - mRootView = mRoot; + mRoot = mRootView; } }; } @@ -212,8 +218,10 @@ public class KeyguardBouncerTest extends SysuiTestCase { verify(mExpansionCallback).onFullyShown(); verify(mExpansionCallback, never()).onStartingToHide(); + verify(mKeyguardHostView, never()).onStartingToHide(); mBouncer.setExpansion(0.9f); verify(mExpansionCallback).onStartingToHide(); + verify(mKeyguardHostView).onStartingToHide(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java index 8ab660c3fca3..3e469073694f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java @@ -56,7 +56,7 @@ public class LightBarControllerTest extends SysuiTestCase { when(mStatusBarIconController.getTransitionsController()).thenReturn( mLightBarTransitionsController); mLightBarController = new LightBarController(mContext, mStatusBarIconController, - mock(BatteryController.class)); + mock(BatteryController.class), mock(NavigationModeController.class)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarTransitionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarTransitionsTest.java index 27a50027cfe2..14c6e9f9624d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarTransitionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NavigationBarTransitionsTest.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -50,9 +51,11 @@ public class NavigationBarTransitionsTest extends SysuiTestCase { mDependency.injectMockDependency(IWindowManager.class); mDependency.injectMockDependency(AssistManager.class); mDependency.injectMockDependency(OverviewProxyService.class); - mDependency.injectMockDependency(NavigationModeController.class); mDependency.injectMockDependency(StatusBarStateController.class); mDependency.injectMockDependency(KeyguardStateController.class); + doReturn(mContext) + .when(mDependency.injectMockDependency(NavigationModeController.class)) + .getCurrentUserContext(); NavigationBarView navBar = spy(new NavigationBarView(mContext, null)); when(navBar.getCurrentView()).thenReturn(navBar); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index dd28687e749c..1afe132f00a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -176,25 +176,43 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase { when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(false); - mNotificationActivityStarter = (new StatusBarNotificationActivityStarter.Builder( - getContext(), mock(CommandQueue.class), () -> mAssistManager, - mEntryManager, mock(HeadsUpManagerPhone.class), - mActivityStarter, mStatusBarService, - mock(StatusBarStateController.class), mStatusBarKeyguardViewManager, - mock(KeyguardManager.class), - mock(IDreamManager.class), mRemoteInputManager, - mock(StatusBarRemoteInputCallback.class), mock(NotificationGroupManager.class), - mock(NotificationLockscreenUserManager.class), - mKeyguardStateController, - mock(NotificationInterruptStateProvider.class), mock(MetricsLogger.class), - mock(LockPatternUtils.class), mHandler, mHandler, mUiBgExecutor, - mActivityIntentHelper, mBubbleController, mShadeController, mFeatureFlags, - mNotifPipeline, mNotifCollection) + mNotificationActivityStarter = + new StatusBarNotificationActivityStarter.Builder( + getContext(), + mock(CommandQueue.class), + mHandler, + mHandler, + mUiBgExecutor, + mEntryManager, + mNotifPipeline, + mNotifCollection, + mock(HeadsUpManagerPhone.class), + mActivityStarter, + mStatusBarService, + mock(StatusBarStateController.class), + mStatusBarKeyguardViewManager, + mock(KeyguardManager.class), + mock(IDreamManager.class), + mBubbleController, + () -> mAssistManager, + mRemoteInputManager, + mock(NotificationGroupManager.class), + mock(NotificationLockscreenUserManager.class), + mShadeController, + mKeyguardStateController, + mock(NotificationInterruptStateProvider.class), + mock(LockPatternUtils.class), + mock(StatusBarRemoteInputCallback.class), + mActivityIntentHelper, + + mFeatureFlags, + mock(MetricsLogger.class), + mock(StatusBarNotificationActivityStarterLogger.class)) .setStatusBar(mStatusBar) - .setNotificationPanelViewController(mock(NotificationPanelViewController.class)) .setNotificationPresenter(mock(NotificationPresenter.class)) - .setActivityLaunchAnimator(mock(ActivityLaunchAnimator.class))) - .build(); + .setNotificationPanelViewController(mock(NotificationPanelViewController.class)) + .setActivityLaunchAnimator(mock(ActivityLaunchAnimator.class)) + .build(); // set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg doAnswer(mCallOnDismiss).when(mActivityStarter).dismissKeyguardThenExecute( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java index 962d77366875..aef454fc1374 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java @@ -32,6 +32,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Instrumentation; @@ -222,7 +223,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { ArgumentCaptor<ConnectivityManager.NetworkCallback> callbackArg = ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class); - Mockito.verify(mMockCm, atLeastOnce()) + verify(mMockCm, atLeastOnce()) .registerDefaultNetworkCallback(callbackArg.capture(), isA(Handler.class)); mNetworkCallback = callbackArg.getValue(); assertNotNull(mNetworkCallback); @@ -384,7 +385,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { } protected void verifyHasNoSims(boolean hasNoSimsVisible) { - Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setNoSims( + verify(mCallbackHandler, Mockito.atLeastOnce()).setNoSims( eq(hasNoSimsVisible), eq(false)); } @@ -395,7 +396,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { ArgumentCaptor<Boolean> dataInArg = ArgumentCaptor.forClass(Boolean.class); ArgumentCaptor<Boolean> dataOutArg = ArgumentCaptor.forClass(Boolean.class); - Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators( + verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators( any(), iconArg.capture(), anyInt(), @@ -429,7 +430,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { ArgumentCaptor<Integer> typeIconArg = ArgumentCaptor.forClass(Integer.class); // TODO: Verify all fields. - Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators( + verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators( iconArg.capture(), any(), typeIconArg.capture(), @@ -475,7 +476,7 @@ public class NetworkControllerBaseTest extends SysuiTestCase { ArgumentCaptor<CharSequence> typeContentDescriptionHtmlArg = ArgumentCaptor.forClass(CharSequence.class); - Mockito.verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators( + verify(mCallbackHandler, Mockito.atLeastOnce()).setMobileDataIndicators( iconArg.capture(), qsIconArg.capture(), typeIconArg.capture(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java index 9c250c5e8e16..988e02203843 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerWifiTest.java @@ -2,12 +2,14 @@ package com.android.systemui.statusbar.policy; import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Intent; +import android.net.ConnectivityManager; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.wifi.WifiInfo; @@ -171,6 +173,32 @@ public class NetworkControllerWifiTest extends NetworkControllerBaseTest { verifyLastWifiIcon(true, WifiIcons.WIFI_SIGNAL_STRENGTH[1][testLevel]); } + @Test + public void testFetchInitialData() { + mNetworkController.mWifiSignalController.fetchInitialState(); + Mockito.verify(mMockWm).getWifiState(); + Mockito.verify(mMockCm).getNetworkInfo(ConnectivityManager.TYPE_WIFI); + } + + @Test + public void testFetchInitialData_correctValues() { + String testSsid = "TEST"; + + when(mMockWm.getWifiState()).thenReturn(WifiManager.WIFI_STATE_ENABLED); + NetworkInfo networkInfo = mock(NetworkInfo.class); + when(networkInfo.isConnected()).thenReturn(true); + when(mMockCm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)).thenReturn(networkInfo); + WifiInfo wifiInfo = mock(WifiInfo.class); + when(wifiInfo.getSSID()).thenReturn(testSsid); + when(mMockWm.getConnectionInfo()).thenReturn(wifiInfo); + + mNetworkController.mWifiSignalController.fetchInitialState(); + + assertTrue(mNetworkController.mWifiSignalController.mCurrentState.enabled); + assertTrue(mNetworkController.mWifiSignalController.mCurrentState.connected); + assertEquals(testSsid, mNetworkController.mWifiSignalController.mCurrentState.ssid); + } + protected void setWifiActivity(int activity) { // TODO: Not this, because this variable probably isn't sticking around. mNetworkController.mWifiSignalController.setActivity(activity); diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/RepeatableExecutorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/RepeatableExecutorTest.java new file mode 100644 index 000000000000..00f37ae6f6cb --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/RepeatableExecutorTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.concurrency; + +import static com.google.common.truth.Truth.assertThat; + +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class RepeatableExecutorTest extends SysuiTestCase { + + private static final int DELAY = 100; + + private FakeSystemClock mFakeClock; + private FakeExecutor mFakeExecutor; + private RepeatableExecutor mExecutor; + private CountingTask mCountingTask; + + @Before + public void setUp() throws Exception { + mFakeClock = new FakeSystemClock(); + mFakeExecutor = new FakeExecutor(mFakeClock); + mCountingTask = new CountingTask(); + mExecutor = new RepeatableExecutorImpl(mFakeExecutor); + } + + /** + * Test FakeExecutor that receives non-delayed items to execute. + */ + @Test + public void testExecute() { + mExecutor.execute(mCountingTask); + mFakeExecutor.runAllReady(); + assertThat(mCountingTask.getCount()).isEqualTo(1); + } + + @Test + public void testRepeats() { + // GIVEN that a command is queued to repeat + mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY); + // WHEN The clock advances and the task is run + mFakeExecutor.advanceClockToNext(); + mFakeExecutor.runAllReady(); + // THEN another task is queued + assertThat(mCountingTask.getCount()).isEqualTo(1); + assertThat(mFakeExecutor.numPending()).isEqualTo(1); + } + + @Test + public void testNoExecutionBeforeStartDelay() { + // WHEN a command is queued with a start delay + mExecutor.executeRepeatedly(mCountingTask, 2 * DELAY, DELAY); + mFakeExecutor.runAllReady(); + // THEN then it doesn't run immediately + assertThat(mCountingTask.getCount()).isEqualTo(0); + assertThat(mFakeExecutor.numPending()).isEqualTo(1); + } + + @Test + public void testExecuteAfterStartDelay() { + // GIVEN that a command is queued to repeat with a longer start delay + mExecutor.executeRepeatedly(mCountingTask, 2 * DELAY, DELAY); + // WHEN the clock advances the start delay + mFakeClock.advanceTime(2 * DELAY); + mFakeExecutor.runAllReady(); + // THEN the command has run and another task is queued + assertThat(mCountingTask.getCount()).isEqualTo(1); + assertThat(mFakeExecutor.numPending()).isEqualTo(1); + } + + @Test + public void testExecuteWithZeroStartDelay() { + // WHEN a command is queued with no start delay + mExecutor.executeRepeatedly(mCountingTask, 0L, DELAY); + mFakeExecutor.runAllReady(); + // THEN the command has run and another task is queued + assertThat(mCountingTask.getCount()).isEqualTo(1); + assertThat(mFakeExecutor.numPending()).isEqualTo(1); + } + + @Test + public void testAdvanceTimeTwice() { + // GIVEN that a command is queued to repeat + mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY); + // WHEN the clock advances the time DELAY twice + mFakeClock.advanceTime(DELAY); + mFakeExecutor.runAllReady(); + mFakeClock.advanceTime(DELAY); + mFakeExecutor.runAllReady(); + // THEN the command has run twice and another task is queued + assertThat(mCountingTask.getCount()).isEqualTo(2); + assertThat(mFakeExecutor.numPending()).isEqualTo(1); + } + + @Test + public void testCancel() { + // GIVEN that a scheduled command has been cancelled + Runnable cancel = mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY); + cancel.run(); + // WHEN the clock advances the time DELAY + mFakeClock.advanceTime(DELAY); + mFakeExecutor.runAllReady(); + // THEN the comamnd has not run and no further tasks are queued + assertThat(mCountingTask.getCount()).isEqualTo(0); + assertThat(mFakeExecutor.numPending()).isEqualTo(0); + } + + @Test + public void testCancelAfterStart() { + // GIVEN that a command has reapeated a few times + Runnable cancel = mExecutor.executeRepeatedly(mCountingTask, DELAY, DELAY); + mFakeClock.advanceTime(DELAY); + mFakeExecutor.runAllReady(); + // WHEN cancelled and time advances + cancel.run(); + // THEN the command has only run the first time + assertThat(mCountingTask.getCount()).isEqualTo(1); + assertThat(mFakeExecutor.numPending()).isEqualTo(0); + } + + /** + * Runnable used for testing that counts the number of times run() is invoked. + */ + private static class CountingTask implements Runnable { + + private int mRunCount; + + @Override + public void run() { + mRunCount++; + } + + /** Gets the run count. */ + public int getCount() { + return mRunCount; + } + } +} diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 132b6927badd..da9bdf3262d5 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -1023,8 +1023,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ try { mMainHandler.post(PooledLambda.obtainRunnable((nonArg) -> { final ScreenshotGraphicBuffer screenshotBuffer = LocalServices - .getService(DisplayManagerInternal.class) - .screenshotWithoutSecureLayer(displayId); + .getService(DisplayManagerInternal.class).userScreenshot(displayId); if (screenshotBuffer != null) { sendScreenshotSuccess(screenshotBuffer, callback); } else { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index f21f0e73e787..1a72cf023453 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -2732,7 +2732,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { - new AccessibilityShellCommand(this).exec(this, in, out, err, args, + new AccessibilityShellCommand(this, mSystemActionPerformer).exec(this, in, out, err, args, callback, resultReceiver); } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java index 20a11bd9acd3..b36626f9d736 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityShellCommand.java @@ -18,6 +18,8 @@ package com.android.server.accessibility; import android.annotation.NonNull; import android.app.ActivityManager; +import android.os.Binder; +import android.os.Process; import android.os.ShellCommand; import android.os.UserHandle; @@ -28,9 +30,12 @@ import java.io.PrintWriter; */ final class AccessibilityShellCommand extends ShellCommand { final @NonNull AccessibilityManagerService mService; + final @NonNull SystemActionPerformer mSystemActionPerformer; - AccessibilityShellCommand(@NonNull AccessibilityManagerService service) { + AccessibilityShellCommand(@NonNull AccessibilityManagerService service, + @NonNull SystemActionPerformer systemActionPerformer) { mService = service; + mSystemActionPerformer = systemActionPerformer; } @Override @@ -45,6 +50,9 @@ final class AccessibilityShellCommand extends ShellCommand { case "set-bind-instant-service-allowed": { return runSetBindInstantServiceAllowed(); } + case "call-system-action": { + return runCallSystemAction(); + } } return -1; } @@ -74,6 +82,22 @@ final class AccessibilityShellCommand extends ShellCommand { return 0; } + private int runCallSystemAction() { + final int callingUid = Binder.getCallingUid(); + if (callingUid != Process.ROOT_UID + && callingUid != Process.SYSTEM_UID + && callingUid != Process.SHELL_UID) { + return -1; + } + final String option = getNextArg(); + if (option != null) { + int actionId = Integer.parseInt(option); + mSystemActionPerformer.performSystemAction(actionId); + return 0; + } + return -1; + } + private Integer parseUserId() { final String option = getNextOption(); if (option != null) { @@ -97,5 +121,7 @@ final class AccessibilityShellCommand extends ShellCommand { pw.println(" Set whether binding to services provided by instant apps is allowed."); pw.println(" get-bind-instant-service-allowed [--user <USER_ID>]"); pw.println(" Get whether binding to services provided by instant apps is allowed."); + pw.println(" call-system-action <ACTION_ID>"); + pw.println(" Calls the system action with the given action id."); } }
\ No newline at end of file diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java index ef8d524bee25..a1fc3fa1857d 100644 --- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java +++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java @@ -16,6 +16,8 @@ package com.android.server.accessibility; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS; + import android.accessibilityservice.AccessibilityService; import android.app.PendingIntent; import android.app.RemoteAction; @@ -301,7 +303,11 @@ public class SystemActionPerformer { return lockScreen(); case AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT: return takeScreenshot(); + case AccessibilityService.GLOBAL_ACTION_KEYCODE_HEADSETHOOK : + sendDownAndUpKeyEvents(KeyEvent.KEYCODE_HEADSETHOOK); + return true; default: + Slog.e(TAG, "Invalid action id: " + actionId); return false; } } finally { @@ -395,7 +401,8 @@ public class SystemActionPerformer { ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null) ? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext); screenshotHelper.takeScreenshot(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN, - true, true, new Handler(Looper.getMainLooper()), null); + true, true, SCREENSHOT_ACCESSIBILITY_ACTIONS, + new Handler(Looper.getMainLooper()), null); return true; } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 9d1ad4239a24..35089d6f5de7 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -117,6 +117,7 @@ import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.function.Function; /** * A session for a given activity. @@ -717,10 +718,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState Consumer<InlineSuggestionsRequest> inlineSuggestionsRequestConsumer = mAssistReceiver.newAutofillRequestLocked(/*isInlineRequest=*/ true); if (inlineSuggestionsRequestConsumer != null) { + final AutofillId focusedId = mCurrentViewId; remoteRenderService.getInlineSuggestionsRendererInfo( new RemoteCallback((extras) -> { mInlineSessionController.onCreateInlineSuggestionsRequestLocked( - mCurrentViewId, inlineSuggestionsRequestConsumer, extras); + focusedId, inlineSuggestionsRequestConsumer, extras); } )); } @@ -2786,6 +2788,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState */ private boolean requestShowInlineSuggestionsLocked(@NonNull FillResponse response, @Nullable String filterText) { + if (mCurrentViewId == null) { + Log.w(TAG, "requestShowInlineSuggestionsLocked(): no view currently focused"); + return false; + } + final AutofillId focusedId = mCurrentViewId; + final Optional<InlineSuggestionsRequest> inlineSuggestionsRequest = mInlineSessionController.getInlineSuggestionsRequestLocked(); if (!inlineSuggestionsRequest.isPresent()) { @@ -2800,17 +2808,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return false; } - final ViewState currentView = mViewStates.get(mCurrentViewId); + final ViewState currentView = mViewStates.get(focusedId); if ((currentView.getState() & ViewState.STATE_INLINE_DISABLED) != 0) { response.getDatasets().clear(); } InlineSuggestionsResponse inlineSuggestionsResponse = InlineSuggestionFactory.createInlineSuggestionsResponse( - inlineSuggestionsRequest.get(), response, filterText, mCurrentViewId, + inlineSuggestionsRequest.get(), response, filterText, focusedId, this, () -> { synchronized (mLock) { mInlineSessionController.hideInlineSuggestionsUiLocked( - mCurrentViewId); + focusedId); } }, remoteRenderService); if (inlineSuggestionsResponse == null) { @@ -2818,7 +2826,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return false; } - return mInlineSessionController.onInlineSuggestionsResponseLocked(mCurrentViewId, + return mInlineSessionController.onInlineSuggestionsResponseLocked(focusedId, inlineSuggestionsResponse); } @@ -3072,19 +3080,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final boolean isWhitelisted = mService .isWhitelistedForAugmentedAutofillLocked(mComponentName); - final String historyItem = - "aug:id=" + id + " u=" + uid + " m=" + mode - + " a=" + ComponentName.flattenToShortString(mComponentName) - + " f=" + mCurrentViewId - + " s=" + remoteService.getComponentName() - + " w=" + isWhitelisted; - mService.getMaster().logRequestLocked(historyItem); - if (!isWhitelisted) { if (sVerbose) { Slog.v(TAG, "triggerAugmentedAutofillLocked(): " + ComponentName.flattenToShortString(mComponentName) + " not whitelisted "); } + logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(), + mCurrentViewId, isWhitelisted, /*isInline*/null); return null; } @@ -3107,26 +3109,29 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState remoteService.getComponentName().getPackageName()); mAugmentedRequestsLogs.add(log); - final AutofillId focusedId = AutofillId.withoutSession(mCurrentViewId); + final AutofillId focusedId = mCurrentViewId; + final Function<InlineSuggestionsResponse, Boolean> inlineSuggestionsResponseCallback = + response -> { + synchronized (mLock) { + return mInlineSessionController.onInlineSuggestionsResponseLocked( + focusedId, response); + } + }; final Consumer<InlineSuggestionsRequest> requestAugmentedAutofill = (inlineSuggestionsRequest) -> { - remoteService.onRequestAutofillLocked(id, mClient, taskId, mComponentName, - focusedId, - currentValue, inlineSuggestionsRequest, - /*inlineSuggestionsCallback=*/ - response -> { - synchronized (mLock) { - return mInlineSessionController - .onInlineSuggestionsResponseLocked( - mCurrentViewId, response); - } - }, - /*onErrorCallback=*/ () -> { - synchronized (mLock) { - cancelAugmentedAutofillLocked(); - } - }, mService.getRemoteInlineSuggestionRenderServiceLocked()); + synchronized (mLock) { + logAugmentedAutofillRequestLocked(mode, remoteService.getComponentName(), + focusedId, isWhitelisted, inlineSuggestionsRequest != null); + remoteService.onRequestAutofillLocked(id, mClient, taskId, mComponentName, + AutofillId.withoutSession(focusedId), currentValue, + inlineSuggestionsRequest, inlineSuggestionsResponseCallback, + /*onErrorCallback=*/ () -> { + synchronized (mLock) { + cancelAugmentedAutofillLocked(); + } + }, mService.getRemoteInlineSuggestionRenderServiceLocked()); + } }; // When the inline suggestion render service is available, there are 2 cases when @@ -3143,9 +3148,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (sDebug) Slog.d(TAG, "Create inline request for augmented autofill"); remoteRenderService.getInlineSuggestionsRendererInfo(new RemoteCallback( (extras) -> { - mInlineSessionController.onCreateInlineSuggestionsRequestLocked( - mCurrentViewId, /*requestConsumer=*/ requestAugmentedAutofill, - extras); + synchronized (mLock) { + mInlineSessionController.onCreateInlineSuggestionsRequestLocked( + focusedId, /*requestConsumer=*/ requestAugmentedAutofill, + extras); + } }, mHandler)); } else { requestAugmentedAutofill.accept( @@ -3158,6 +3165,20 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } @GuardedBy("mLock") + private void logAugmentedAutofillRequestLocked(int mode, + ComponentName augmentedRemoteServiceName, AutofillId focusedId, boolean isWhitelisted, + Boolean isInline) { + final String historyItem = + "aug:id=" + id + " u=" + uid + " m=" + mode + + " a=" + ComponentName.flattenToShortString(mComponentName) + + " f=" + focusedId + + " s=" + augmentedRemoteServiceName + + " w=" + isWhitelisted + + " i=" + isInline; + mService.getMaster().logRequestLocked(historyItem); + } + + @GuardedBy("mLock") private void cancelAugmentedAutofillLocked() { final RemoteAugmentedAutofillService remoteService = mService .getRemoteAugmentedAutofillServiceLocked(); diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index c27ec66b5db3..81de29c4ee4d 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -480,6 +480,12 @@ public abstract class PackageManagerInternal { public abstract void pruneInstantApps(); /** + * Prunes the cache of the APKs in the given APEXes. + * @param apexPackages The list of APEX packages that may contain APK-in-APEX. + */ + public abstract void pruneCachedApksInApex(@NonNull List<PackageInfo> apexPackages); + + /** * @return The SetupWizard package name. */ public abstract String getSetupWizardPackageName(); @@ -977,4 +983,9 @@ public abstract class PackageManagerInternal { * Returns if a package name is a valid system package. */ public abstract boolean isSystemPackage(@NonNull String packageName); + + /** + * Unblocks uninstall for all packages for the user. + */ + public abstract void clearBlockUninstallForUser(@UserIdInt int userId); } diff --git a/services/core/java/com/android/server/AlarmManagerService.java b/services/core/java/com/android/server/AlarmManagerService.java index ce651100699f..4009cafd04fb 100644 --- a/services/core/java/com/android/server/AlarmManagerService.java +++ b/services/core/java/com/android/server/AlarmManagerService.java @@ -603,8 +603,7 @@ class AlarmManagerService extends SystemService { } pw.print(KEY_APP_STANDBY_RESTRICTED_QUOTA); pw.print("="); - TimeUtils.formatDuration(APP_STANDBY_RESTRICTED_QUOTA, pw); - pw.println(); + pw.println(APP_STANDBY_RESTRICTED_QUOTA); pw.print(KEY_APP_STANDBY_RESTRICTED_WINDOW); pw.print("="); TimeUtils.formatDuration(APP_STANDBY_RESTRICTED_WINDOW, pw); diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index 2bbf27849005..97a5cfe6006d 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -1146,7 +1146,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { && registrationLimit >= 1 && numRecordsForPid >= registrationLimit) { String errorMsg = "Pid " + callingPid + " has exceeded the number of permissible" - + "registered listeners. Ignoring request to add."; + + " registered listeners. Ignoring request to add."; loge(errorMsg); if (mConfigurationProvider .isRegistrationLimitEnabledInPlatformCompat(callingUid)) { @@ -1157,7 +1157,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { // Log the warning independently of the dynamically set limit -- apps shouldn't be // doing this regardless of whether we're throwing them an exception for it. Rlog.w(TAG, "Pid " + callingPid + " has exceeded half the number of permissible" - + "registered listeners. Now at " + numRecordsForPid); + + " registered listeners. Now at " + numRecordsForPid); } r = new Record(); diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java index ac4a42ca7024..e066d99147ba 100644 --- a/services/core/java/com/android/server/VibratorService.java +++ b/services/core/java/com/android/server/VibratorService.java @@ -1034,6 +1034,9 @@ public class VibratorService extends IVibratorService.Stub VibrationEffect.Waveform waveform = (VibrationEffect.Waveform) vib.effect; waveform = waveform.resolve(mDefaultVibrationAmplitude); scaledEffect = waveform.scale(scale.gamma, scale.maxAmplitude); + } else if (vib.effect instanceof VibrationEffect.Composed) { + VibrationEffect.Composed composed = (VibrationEffect.Composed) vib.effect; + scaledEffect = composed.scale(scale.gamma, scale.maxAmplitude); } else { Slog.w(TAG, "Unable to apply intensity scaling, unknown VibrationEffect type"); } diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index dbad562c0271..b647818e3f7a 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -1013,9 +1013,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub @Override public void noteNetworkInterfaceType(String iface, int networkType) { enforceCallingPermission(); - synchronized (mStats) { - mStats.noteNetworkInterfaceTypeLocked(iface, networkType); - } + mStats.noteNetworkInterfaceType(iface, networkType); } @Override diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index b4f7cdbd5694..02d499fbd81f 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -28,6 +28,8 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_SHOUL import static android.hardware.display.DisplayViewport.VIEWPORT_EXTERNAL; import static android.hardware.display.DisplayViewport.VIEWPORT_INTERNAL; import static android.hardware.display.DisplayViewport.VIEWPORT_VIRTUAL; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; import android.Manifest; import android.annotation.NonNull; @@ -1363,8 +1365,7 @@ public final class DisplayManagerService extends SystemService { return null; } - private SurfaceControl.ScreenshotGraphicBuffer screenshotInternal(int displayId, - boolean captureSecureLayer) { + private SurfaceControl.ScreenshotGraphicBuffer systemScreenshotInternal(int displayId) { synchronized (mSyncRoot) { final IBinder token = getDisplayToken(displayId); if (token == null) { @@ -1376,15 +1377,42 @@ public final class DisplayManagerService extends SystemService { } final DisplayInfo displayInfo = logicalDisplay.getDisplayInfoLocked(); - if (captureSecureLayer) { - return SurfaceControl.screenshotToBufferWithSecureLayersUnsafe(token, new Rect(), - displayInfo.getNaturalWidth(), displayInfo.getNaturalHeight(), - false /* useIdentityTransform */, 0 /* rotation */); - } else { - return SurfaceControl.screenshotToBuffer(token, new Rect(), - displayInfo.getNaturalWidth(), displayInfo.getNaturalHeight(), - false /* useIdentityTransform */, 0 /* rotation */); + return SurfaceControl.screenshotToBufferWithSecureLayersUnsafe(token, new Rect(), + displayInfo.getNaturalWidth(), displayInfo.getNaturalHeight(), + false /* useIdentityTransform */, 0 /* rotation */); + } + } + + private SurfaceControl.ScreenshotGraphicBuffer userScreenshotInternal(int displayId) { + synchronized (mSyncRoot) { + final IBinder token = getDisplayToken(displayId); + if (token == null) { + return null; + } + final LogicalDisplay logicalDisplay = mLogicalDisplays.get(displayId); + if (logicalDisplay == null) { + return null; + } + + final DisplayInfo displayInfo = logicalDisplay.getDisplayInfoLocked(); + // Takes screenshot based on current device orientation. + final Display display = DisplayManagerGlobal.getInstance() + .getRealDisplay(displayId); + if (display == null) { + return null; } + final Point displaySize = new Point(); + display.getRealSize(displaySize); + + int rotation = displayInfo.rotation; + // TODO (b/153382624) : This workaround solution would be removed after + // SurfaceFlinger fixes the inconsistency with rotation direction issue. + if (rotation == ROTATION_90 || rotation == ROTATION_270) { + rotation = (rotation == ROTATION_90) ? ROTATION_270 : ROTATION_90; + } + + return SurfaceControl.screenshotToBuffer(token, new Rect(), displaySize.x, + displaySize.y, false /* useIdentityTransform */, rotation /* rotation */); } } @@ -2502,13 +2530,13 @@ public final class DisplayManagerService extends SystemService { } @Override - public SurfaceControl.ScreenshotGraphicBuffer screenshot(int displayId) { - return screenshotInternal(displayId, true); + public SurfaceControl.ScreenshotGraphicBuffer systemScreenshot(int displayId) { + return systemScreenshotInternal(displayId); } @Override - public SurfaceControl.ScreenshotGraphicBuffer screenshotWithoutSecureLayer(int displayId) { - return screenshotInternal(displayId, false); + public SurfaceControl.ScreenshotGraphicBuffer userScreenshot(int displayId) { + return userScreenshotInternal(displayId); } @Override diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java index fbee6f4bcbf0..848019738abe 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsStrongAuth.java @@ -26,6 +26,7 @@ import android.app.admin.DevicePolicyManager; import android.app.trust.IStrongAuthTracker; import android.content.Context; import android.os.Handler; +import android.os.Looper; import android.os.Message; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -36,6 +37,7 @@ import android.util.Slog; import android.util.SparseBooleanArray; import android.util.SparseIntArray; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.widget.LockPatternUtils.StrongAuthTracker; @@ -57,11 +59,14 @@ public class LockSettingsStrongAuth { private static final int MSG_STRONG_BIOMETRIC_UNLOCK = 8; private static final int MSG_SCHEDULE_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT = 9; - private static final String STRONG_AUTH_TIMEOUT_ALARM_TAG = + @VisibleForTesting + protected static final String STRONG_AUTH_TIMEOUT_ALARM_TAG = "LockSettingsStrongAuth.timeoutForUser"; - private static final String NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG = + @VisibleForTesting + protected static final String NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG = "LockSettingsPrimaryAuth.nonStrongBiometricTimeoutForUser"; - private static final String NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG = + @VisibleForTesting + protected static final String NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG = "LockSettingsPrimaryAuth.nonStrongBiometricIdleTimeoutForUser"; /** @@ -73,28 +78,71 @@ public class LockSettingsStrongAuth { 4 * 60 * 60 * 1000; // 4h private final RemoteCallbackList<IStrongAuthTracker> mTrackers = new RemoteCallbackList<>(); - private final SparseIntArray mStrongAuthForUser = new SparseIntArray(); - private final SparseBooleanArray mIsNonStrongBiometricAllowedForUser = new SparseBooleanArray(); - private final ArrayMap<Integer, StrongAuthTimeoutAlarmListener> + @VisibleForTesting + protected final SparseIntArray mStrongAuthForUser = new SparseIntArray(); + @VisibleForTesting + protected final SparseBooleanArray mIsNonStrongBiometricAllowedForUser = + new SparseBooleanArray(); + @VisibleForTesting + protected final ArrayMap<Integer, StrongAuthTimeoutAlarmListener> mStrongAuthTimeoutAlarmListenerForUser = new ArrayMap<>(); // Track non-strong biometric timeout - private final ArrayMap<Integer, NonStrongBiometricTimeoutAlarmListener> + @VisibleForTesting + protected final ArrayMap<Integer, NonStrongBiometricTimeoutAlarmListener> mNonStrongBiometricTimeoutAlarmListener = new ArrayMap<>(); // Track non-strong biometric idle timeout - private final ArrayMap<Integer, NonStrongBiometricIdleTimeoutAlarmListener> + @VisibleForTesting + protected final ArrayMap<Integer, NonStrongBiometricIdleTimeoutAlarmListener> mNonStrongBiometricIdleTimeoutAlarmListener = new ArrayMap<>(); private final int mDefaultStrongAuthFlags; private final boolean mDefaultIsNonStrongBiometricAllowed = true; private final Context mContext; - - private AlarmManager mAlarmManager; + private final Injector mInjector; + private final AlarmManager mAlarmManager; public LockSettingsStrongAuth(Context context) { + this(context, new Injector()); + } + + @VisibleForTesting + protected LockSettingsStrongAuth(Context context, Injector injector) { mContext = context; - mDefaultStrongAuthFlags = StrongAuthTracker.getDefaultFlags(context); - mAlarmManager = context.getSystemService(AlarmManager.class); + mInjector = injector; + mDefaultStrongAuthFlags = mInjector.getDefaultStrongAuthFlags(context); + mAlarmManager = mInjector.getAlarmManager(context); + } + + /** + * Class for injecting dependencies into LockSettingsStrongAuth. + */ + @VisibleForTesting + public static class Injector { + + /** + * Allows to mock AlarmManager for testing. + */ + @VisibleForTesting + public AlarmManager getAlarmManager(Context context) { + return context.getSystemService(AlarmManager.class); + } + + /** + * Allows to get different default StrongAuthFlags for testing. + */ + @VisibleForTesting + public int getDefaultStrongAuthFlags(Context context) { + return StrongAuthTracker.getDefaultFlags(context); + } + + /** + * Allows to get different triggerAtMillis values when setting alarms for testing. + */ + @VisibleForTesting + public long getNextAlarmTimeMs(long timeout) { + return SystemClock.elapsedRealtime() + timeout; + } } private void handleAddStrongAuthTracker(IStrongAuthTracker tracker) { @@ -186,7 +234,8 @@ public class LockSettingsStrongAuth { private void handleScheduleStrongAuthTimeout(int userId) { final DevicePolicyManager dpm = (DevicePolicyManager) mContext.getSystemService(Context.DEVICE_POLICY_SERVICE); - long when = SystemClock.elapsedRealtime() + dpm.getRequiredStrongAuthTimeout(null, userId); + long nextAlarmTime = + mInjector.getNextAlarmTimeMs(dpm.getRequiredStrongAuthTimeout(null, userId)); // cancel current alarm listener for the user (if there was one) StrongAuthTimeoutAlarmListener alarm = mStrongAuthTimeoutAlarmListenerForUser.get(userId); if (alarm != null) { @@ -196,8 +245,8 @@ public class LockSettingsStrongAuth { mStrongAuthTimeoutAlarmListenerForUser.put(userId, alarm); } // schedule a new alarm listener for the user - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, STRONG_AUTH_TIMEOUT_ALARM_TAG, - alarm, mHandler); + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, + STRONG_AUTH_TIMEOUT_ALARM_TAG, alarm, mHandler); // cancel current non-strong biometric alarm listener for the user (if there was one) cancelNonStrongBiometricAlarmListener(userId); @@ -209,7 +258,7 @@ public class LockSettingsStrongAuth { private void handleScheduleNonStrongBiometricTimeout(int userId) { if (DEBUG) Slog.d(TAG, "handleScheduleNonStrongBiometricTimeout for userId=" + userId); - long when = SystemClock.elapsedRealtime() + DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS; + long nextAlarmTime = mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS); NonStrongBiometricTimeoutAlarmListener alarm = mNonStrongBiometricTimeoutAlarmListener .get(userId); if (alarm != null) { @@ -226,7 +275,7 @@ public class LockSettingsStrongAuth { alarm = new NonStrongBiometricTimeoutAlarmListener(userId); mNonStrongBiometricTimeoutAlarmListener.put(userId, alarm); // schedule a new alarm listener for the user - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG, alarm, mHandler); } @@ -268,7 +317,8 @@ public class LockSettingsStrongAuth { } } - private void setIsNonStrongBiometricAllowed(boolean allowed, int userId) { + @VisibleForTesting + protected void setIsNonStrongBiometricAllowed(boolean allowed, int userId) { if (DEBUG) { Slog.d(TAG, "setIsNonStrongBiometricAllowed for allowed=" + allowed + ", userId=" + userId); @@ -302,7 +352,8 @@ public class LockSettingsStrongAuth { private void handleScheduleNonStrongBiometricIdleTimeout(int userId) { if (DEBUG) Slog.d(TAG, "handleScheduleNonStrongBiometricIdleTimeout for userId=" + userId); - long when = SystemClock.elapsedRealtime() + DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS; + long nextAlarmTime = + mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS); // cancel current alarm listener for the user (if there was one) NonStrongBiometricIdleTimeoutAlarmListener alarm = mNonStrongBiometricIdleTimeoutAlarmListener.get(userId); @@ -315,7 +366,7 @@ public class LockSettingsStrongAuth { } // schedule a new alarm listener for the user if (DEBUG) Slog.d(TAG, "Schedule a new alarm for non-strong biometric idle timeout"); - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, when, + mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextAlarmTime, NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG, alarm, mHandler); } @@ -435,7 +486,8 @@ public class LockSettingsStrongAuth { /** * Alarm of fallback timeout for primary auth */ - private class StrongAuthTimeoutAlarmListener implements OnAlarmListener { + @VisibleForTesting + protected class StrongAuthTimeoutAlarmListener implements OnAlarmListener { private final int mUserId; @@ -452,7 +504,8 @@ public class LockSettingsStrongAuth { /** * Alarm of fallback timeout for non-strong biometric (i.e. weak or convenience) */ - private class NonStrongBiometricTimeoutAlarmListener implements OnAlarmListener { + @VisibleForTesting + protected class NonStrongBiometricTimeoutAlarmListener implements OnAlarmListener { private final int mUserId; @@ -469,7 +522,8 @@ public class LockSettingsStrongAuth { /** * Alarm of idle timeout for non-strong biometric (i.e. weak or convenience biometric) */ - private class NonStrongBiometricIdleTimeoutAlarmListener implements OnAlarmListener { + @VisibleForTesting + protected class NonStrongBiometricIdleTimeoutAlarmListener implements OnAlarmListener { private final int mUserId; @@ -484,7 +538,8 @@ public class LockSettingsStrongAuth { } } - private final Handler mHandler = new Handler() { + @VisibleForTesting + protected final Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java index 28f838044907..3cf22c85f924 100644 --- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java +++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java @@ -215,7 +215,6 @@ class BluetoothRouteProvider { .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED) .setDescription(mContext.getResources().getText( R.string.bluetooth_a2dp_audio_route_name).toString()) - //TODO: Set type correctly (BLUETOOTH_A2DP or HEARING_AID) .setType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP) .setVolumeHandling(MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) .build(); @@ -236,6 +235,8 @@ class BluetoothRouteProvider { // Update volume when the connection state is changed. MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.route) .setConnectionState(state); + builder.setType(btRoute.connectedProfiles.get(BluetoothProfile.HEARING_AID, false) + ? MediaRoute2Info.TYPE_HEARING_AID : MediaRoute2Info.TYPE_BLUETOOTH_A2DP); if (state == MediaRoute2Info.CONNECTION_STATE_CONNECTED) { int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 0d899974cf93..d8bf9edee2b7 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -16,6 +16,8 @@ package com.android.server.media; +import static android.media.MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE; +import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; import static android.media.MediaRoute2ProviderService.REQUEST_ID_NONE; import static android.media.MediaRouter2Utils.getOriginalId; import static android.media.MediaRouter2Utils.getProviderId; @@ -247,6 +249,22 @@ class MediaRouter2ServiceImpl { } } + public void notifySessionHintsForCreatingSession(IMediaRouter2 router, + long uniqueRequestId, MediaRoute2Info route, Bundle sessionHints) { + Objects.requireNonNull(router, "router must not be null"); + Objects.requireNonNull(route, "route must not be null"); + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + notifySessionHintsForCreatingSessionLocked(uniqueRequestId, + router, route, sessionHints); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + public void selectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId, MediaRoute2Info route) { Objects.requireNonNull(router, "router must not be null"); @@ -265,7 +283,6 @@ class MediaRouter2ServiceImpl { } } - public void deselectRouteWithRouter2(IMediaRouter2 router, String uniqueSessionId, MediaRoute2Info route) { Objects.requireNonNull(router, "router must not be null"); @@ -634,12 +651,30 @@ class MediaRouter2ServiceImpl { long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId); routerRecord.mUserRecord.mHandler.sendMessage( - obtainMessage(UserHandler::requestCreateSessionOnHandler, + obtainMessage(UserHandler::requestCreateSessionWithRouter2OnHandler, routerRecord.mUserRecord.mHandler, - uniqueRequestId, routerRecord, /* managerRecord= */ null, route, + uniqueRequestId, routerRecord, route, sessionHints)); } + private void notifySessionHintsForCreatingSessionLocked(long uniqueRequestId, + @NonNull IMediaRouter2 router, + @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { + final IBinder binder = router.asBinder(); + final RouterRecord routerRecord = mAllRouterRecords.get(binder); + + if (routerRecord == null) { + Slog.w(TAG, "requestCreateSessionWithRouter2ByManagerRequestLocked: " + + "Ignoring unknown router."); + return; + } + + routerRecord.mUserRecord.mHandler.sendMessage( + obtainMessage(UserHandler::requestCreateSessionWithManagerOnHandler, + routerRecord.mUserRecord.mHandler, + uniqueRequestId, routerRecord, route, sessionHints)); + } + private void selectRouteWithRouter2Locked(@NonNull IMediaRouter2 router, @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { final IBinder binder = router.asBinder(); @@ -826,12 +861,13 @@ class MediaRouter2ServiceImpl { } long uniqueRequestId = toUniqueRequestId(managerRecord.mManagerId, requestId); - //TODO(b/152851868): Use MediaRouter2's OnCreateSessionListener to send session hints. + + // Before requesting to the provider, get session hints from the media router. + // As a return, media router will request to create a session. routerRecord.mUserRecord.mHandler.sendMessage( - obtainMessage(UserHandler::requestCreateSessionOnHandler, + obtainMessage(UserHandler::getSessionHintsForCreatingSessionOnHandler, routerRecord.mUserRecord.mHandler, - uniqueRequestId, routerRecord, managerRecord, route, - /* sessionHints= */ null)); + uniqueRequestId, routerRecord, managerRecord, route)); } private void selectRouteWithManagerLocked(int requestId, @NonNull IMediaRouter2Manager manager, @@ -1149,7 +1185,6 @@ class MediaRouter2ServiceImpl { this, provider, uniqueRequestId, sessionInfo)); } - @Override public void onSessionUpdated(@NonNull MediaRoute2Provider provider, @NonNull RoutingSessionInfo sessionInfo) { @@ -1267,8 +1302,26 @@ class MediaRouter2ServiceImpl { return -1; } - private void requestCreateSessionOnHandler(long uniqueRequestId, - @NonNull RouterRecord routerRecord, @Nullable ManagerRecord managerRecord, + private void getSessionHintsForCreatingSessionOnHandler(long uniqueRequestId, + @NonNull RouterRecord routerRecord, @NonNull ManagerRecord managerRecord, + @NonNull MediaRoute2Info route) { + SessionCreationRequest request = + new SessionCreationRequest(routerRecord, uniqueRequestId, route, managerRecord); + mSessionCreationRequests.add(request); + + try { + routerRecord.mRouter.getSessionHintsForCreatingSession(uniqueRequestId, route); + } catch (RemoteException ex) { + Slog.w(TAG, "requestGetSessionHintsOnHandler: " + + "Failed to request. Router probably died."); + mSessionCreationRequests.remove(request); + notifyRequestFailedToManager(managerRecord.mManager, + toOriginalRequestId(uniqueRequestId), REASON_UNKNOWN_ERROR); + } + } + + private void requestCreateSessionWithRouter2OnHandler(long uniqueRequestId, + @NonNull RouterRecord routerRecord, @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { final MediaRoute2Provider provider = findProvider(route.getProviderId()); @@ -1281,13 +1334,50 @@ class MediaRouter2ServiceImpl { } SessionCreationRequest request = - new SessionCreationRequest(routerRecord, uniqueRequestId, route, managerRecord); + new SessionCreationRequest(routerRecord, uniqueRequestId, route, null); mSessionCreationRequests.add(request); provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName, route.getOriginalId(), sessionHints); } + private void requestCreateSessionWithManagerOnHandler(long uniqueRequestId, + @NonNull RouterRecord routerRecord, + @NonNull MediaRoute2Info route, @Nullable Bundle sessionHints) { + SessionCreationRequest matchingRequest = null; + for (SessionCreationRequest request : mSessionCreationRequests) { + if (request.mUniqueRequestId == uniqueRequestId) { + matchingRequest = request; + break; + } + } + if (matchingRequest == null) { + Slog.w(TAG, "requestCreateSessionWithKnownRequestOnHandler: " + + "Ignoring an unknown request."); + return; + } + + if (!TextUtils.equals(matchingRequest.mRoute.getId(), route.getId())) { + Slog.w(TAG, "requestCreateSessionWithKnownRequestOnHandler: " + + "The given route is different from the requested route."); + return; + } + + final MediaRoute2Provider provider = findProvider(route.getProviderId()); + if (provider == null) { + Slog.w(TAG, "Ignoring session creation request since no provider found for" + + " given route=" + route); + + mSessionCreationRequests.remove(matchingRequest); + notifyRequestFailedToManager(matchingRequest.mRequestedManagerRecord.mManager, + toOriginalRequestId(uniqueRequestId), REASON_ROUTE_NOT_AVAILABLE); + return; + } + + provider.requestCreateSession(uniqueRequestId, routerRecord.mPackageName, + route.getOriginalId(), sessionHints); + } + // routerRecord can be null if the session is system's or RCN. private void selectRouteOnHandler(long uniqueRequestId, @Nullable RouterRecord routerRecord, @NonNull String uniqueSessionId, @NonNull MediaRoute2Info route) { diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index d6bf9fb4d9b5..bf2cc5e68fac 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -487,6 +487,14 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override + public void notifySessionHintsForCreatingSession(IMediaRouter2 router, + long uniqueRequestId, MediaRoute2Info route, Bundle sessionHints) { + mService2.notifySessionHintsForCreatingSession(router, + uniqueRequestId, route, sessionHints); + } + + // Binder call + @Override public void selectRouteWithRouter2(IMediaRouter2 router, String sessionId, MediaRoute2Info route) { mService2.selectRouteWithRouter2(router, sessionId, route); diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 1345e3759d2f..41d7fff52a91 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -19,6 +19,11 @@ package com.android.server.media; import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO; import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_DOCK; +import static android.media.MediaRoute2Info.TYPE_HDMI; +import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -194,19 +199,27 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { private void updateDeviceRoute(AudioRoutesInfo newRoutes) { int name = R.string.default_audio_route_name; + int type = TYPE_BUILTIN_SPEAKER; if (newRoutes != null) { mCurAudioRoutesInfo.mainType = newRoutes.mainType; - if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0 - || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { + if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) { + type = TYPE_WIRED_HEADPHONES; + name = com.android.internal.R.string.default_audio_route_name_headphones; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { + type = TYPE_WIRED_HEADSET; name = com.android.internal.R.string.default_audio_route_name_headphones; } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { + type = TYPE_DOCK; name = com.android.internal.R.string.default_audio_route_name_dock_speakers; } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { + type = TYPE_HDMI; name = com.android.internal.R.string.default_audio_route_name_hdmi; } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) { + type = TYPE_USB_DEVICE; name = com.android.internal.R.string.default_audio_route_name_usb; } } + mDeviceRoute = new MediaRoute2Info.Builder( DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) .setVolumeHandling(mAudioManager.isVolumeFixed() @@ -214,8 +227,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) .setVolume(mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC)) - //TODO: Guess the exact type using AudioDevice - .setType(TYPE_BUILTIN_SPEAKER) + .setType(type) .addFeature(FEATURE_LIVE_AUDIO) .addFeature(FEATURE_LIVE_VIDEO) .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index d432fc83b52a..3e6d6f5fe192 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -819,7 +819,9 @@ public class PreferencesHelper implements RankingConfig { } if (fromTargetApp) { channel.setLockscreenVisibility(r.visibility); - channel.setAllowBubbles(existing != null && existing.canBubble()); + channel.setAllowBubbles(existing != null + ? existing.getAllowBubbles() + : NotificationChannel.DEFAULT_ALLOW_BUBBLE); } clearLockedFieldsLocked(channel); channel.setImportanceLockedByOEM(r.oemLockedImportance); @@ -1704,7 +1706,7 @@ public class PreferencesHelper implements RankingConfig { if (original.canShowBadge() != update.canShowBadge()) { update.lockFields(NotificationChannel.USER_LOCKED_SHOW_BADGE); } - if (original.canBubble() != update.canBubble()) { + if (original.getAllowBubbles() != update.getAllowBubbles()) { update.lockFields(NotificationChannel.USER_LOCKED_ALLOW_BUBBLE); } } diff --git a/services/core/java/com/android/server/notification/ShortcutHelper.java b/services/core/java/com/android/server/notification/ShortcutHelper.java index 1d4843822931..96da649350b0 100644 --- a/services/core/java/com/android/server/notification/ShortcutHelper.java +++ b/services/core/java/com/android/server/notification/ShortcutHelper.java @@ -21,7 +21,6 @@ import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC; import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED; import android.annotation.NonNull; -import android.content.Intent; import android.content.IntentFilter; import android.content.pm.LauncherApps; import android.content.pm.ShortcutInfo; @@ -41,9 +40,18 @@ import java.util.List; /** * Helper for querying shortcuts. */ -class ShortcutHelper { +public class ShortcutHelper { private static final String TAG = "ShortcutHelper"; + private static final IntentFilter SHARING_FILTER = new IntentFilter(); + static { + try { + SHARING_FILTER.addDataType("*/*"); + } catch (IntentFilter.MalformedMimeTypeException e) { + Slog.e(TAG, "Bad mime type", e); + } + } + /** * Listener to call when a shortcut we're tracking has been removed. */ @@ -54,7 +62,6 @@ class ShortcutHelper { private LauncherApps mLauncherAppsService; private ShortcutListener mShortcutListener; private ShortcutServiceInternal mShortcutServiceInternal; - private IntentFilter mSharingFilter; // Key: packageName Value: <shortcutId, notifId> private HashMap<String, HashMap<String, String>> mActiveShortcutBubbles = new HashMap<>(); @@ -122,12 +129,6 @@ class ShortcutHelper { ShortcutServiceInternal shortcutServiceInternal) { mLauncherAppsService = launcherApps; mShortcutListener = listener; - mSharingFilter = new IntentFilter(); - try { - mSharingFilter.addDataType("*/*"); - } catch (IntentFilter.MalformedMimeTypeException e) { - Slog.e(TAG, "Bad mime type", e); - } mShortcutServiceInternal = shortcutServiceInternal; } @@ -142,7 +143,21 @@ class ShortcutHelper { } /** - * Only returns shortcut info if it's found and if it's {@link ShortcutInfo#isLongLived()}. + * Returns whether the given shortcut info is a conversation shortcut. + */ + public static boolean isConversationShortcut( + ShortcutInfo shortcutInfo, ShortcutServiceInternal mShortcutServiceInternal, + int callingUserId) { + if (shortcutInfo == null || !shortcutInfo.isLongLived() || !shortcutInfo.isEnabled()) { + return false; + } + return mShortcutServiceInternal.isSharingShortcut(callingUserId, "android", + shortcutInfo.getPackage(), shortcutInfo.getId(), shortcutInfo.getUserId(), + SHARING_FILTER); + } + + /** + * Only returns shortcut info if it's found and if it's a conversation shortcut. */ ShortcutInfo getValidShortcutInfo(String shortcutId, String packageName, UserHandle user) { if (mLauncherAppsService == null) { @@ -161,11 +176,7 @@ class ShortcutHelper { ShortcutInfo info = shortcuts != null && shortcuts.size() > 0 ? shortcuts.get(0) : null; - if (info == null || !info.isLongLived() || !info.isEnabled()) { - return null; - } - if (mShortcutServiceInternal.isSharingShortcut(user.getIdentifier(), - "android", packageName, shortcutId, user.getIdentifier(), mSharingFilter)) { + if (isConversationShortcut(info, mShortcutServiceInternal, user.getIdentifier())) { return info; } return null; diff --git a/services/core/java/com/android/server/pm/AppsFilter.java b/services/core/java/com/android/server/pm/AppsFilter.java index 7c47cf0450d4..d36d038058f1 100644 --- a/services/core/java/com/android/server/pm/AppsFilter.java +++ b/services/core/java/com/android/server/pm/AppsFilter.java @@ -612,7 +612,7 @@ public class AppsFilter { } final int insert = ~loc; System.arraycopy(appIds, insert, buffer, 0, whitelistSize - insert); - appIds[insert] = existingUid; + appIds[insert] = existingAppId; System.arraycopy(buffer, 0, appIds, insert + 1, whitelistSize - insert); whitelistSize++; } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 2221644bff47..6b1ef3acdf61 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -150,7 +150,6 @@ import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Set; @@ -2099,11 +2098,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { continue; } - mResolvedInstructionSets.add(archSubDir.getName()); - List<File> oatFiles = Arrays.asList(archSubDir.listFiles()); - if (!oatFiles.isEmpty()) { - mResolvedInheritedFiles.addAll(oatFiles); + File[] files = archSubDir.listFiles(); + if (files == null || files.length == 0) { + continue; } + + mResolvedInstructionSets.add(archSubDir.getName()); + mResolvedInheritedFiles.addAll(Arrays.asList(files)); } } } @@ -2117,7 +2118,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (!libDir.exists() || !libDir.isDirectory()) { continue; } - final List<File> libDirsToInherit = new LinkedList<>(); + final List<String> libDirsToInherit = new ArrayList<>(); + final List<File> libFilesToInherit = new ArrayList<>(); for (File archSubDir : libDir.listFiles()) { if (!archSubDir.isDirectory()) { continue; @@ -2129,14 +2131,24 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { Slog.e(TAG, "Skipping linking of native library directory!", e); // shouldn't be possible, but let's avoid inheriting these to be safe libDirsToInherit.clear(); + libFilesToInherit.clear(); break; } - if (!mResolvedNativeLibPaths.contains(relLibPath)) { - mResolvedNativeLibPaths.add(relLibPath); + + File[] files = archSubDir.listFiles(); + if (files == null || files.length == 0) { + continue; + } + + libDirsToInherit.add(relLibPath); + libFilesToInherit.addAll(Arrays.asList(files)); + } + for (String subDir : libDirsToInherit) { + if (!mResolvedNativeLibPaths.contains(subDir)) { + mResolvedNativeLibPaths.add(subDir); } - libDirsToInherit.addAll(Arrays.asList(archSubDir.listFiles())); } - mResolvedInheritedFiles.addAll(libDirsToInherit); + mResolvedInheritedFiles.addAll(libFilesToInherit); } } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 07078f26d0b4..7adafe3ed658 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -351,6 +351,7 @@ import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.DexoptOptions; import com.android.server.pm.dex.PackageDexUsage; import com.android.server.pm.dex.ViewCompiler; +import com.android.server.pm.parsing.PackageCacher; import com.android.server.pm.parsing.PackageInfoUtils; import com.android.server.pm.parsing.PackageParser2; import com.android.server.pm.parsing.library.PackageBackwardCompatibility; @@ -1346,13 +1347,6 @@ public class PackageManagerService extends IPackageManager.Stub int updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; boolean needUpdate = false; - if (DEBUG_DOMAIN_VERIFICATION) { - Slog.d(TAG, - "Updating IntentFilterVerificationInfo for package " + packageName - + " verificationId:" + verificationId - + " verified=" + verified); - } - // In a success case, we promote from undefined or ASK to ALWAYS. This // supports a flow where the app fails validation but then ships an updated // APK that passes, and therefore deserves to be in ALWAYS. @@ -17639,65 +17633,120 @@ public class PackageManagerService extends IPackageManager.Stub + " Activities needs verification ..."); int count = 0; - + boolean handlesWebUris = false; + ArraySet<String> domains = new ArraySet<>(); + final boolean previouslyVerified; + boolean hostSetExpanded = false; + boolean needToRunVerify = false; synchronized (mLock) { // If this is a new install and we see that we've already run verification for this // package, we have nothing to do: it means the state was restored from backup. - if (!replacing) { - IntentFilterVerificationInfo ivi = - mSettings.getIntentFilterVerificationLPr(packageName); - if (ivi != null) { - if (DEBUG_DOMAIN_VERIFICATION) { - Slog.i(TAG, "Package " + packageName+ " already verified: status=" - + ivi.getStatusString()); - } - return; + IntentFilterVerificationInfo ivi = + mSettings.getIntentFilterVerificationLPr(packageName); + previouslyVerified = (ivi != null); + if (!replacing && previouslyVerified) { + if (DEBUG_DOMAIN_VERIFICATION) { + Slog.i(TAG, "Package " + packageName + " already verified: status=" + + ivi.getStatusString()); } + return; } - // If any filters need to be verified, then all need to be. - boolean needToVerify = false; + if (DEBUG_DOMAIN_VERIFICATION) { + Slog.i(TAG, " Previous verified hosts: " + + (ivi == null ? "[none]" : ivi.getDomainsString())); + } + + // If any filters need to be verified, then all need to be. In addition, we need to + // know whether an updating app has any web navigation intent filters, to re- + // examine handling policy even if not re-verifying. + final boolean needsVerification = needsNetworkVerificationLPr(packageName); for (ParsedActivity a : activities) { for (ParsedIntentInfo filter : a.getIntents()) { - if (filter.needsVerification() - && needsNetworkVerificationLPr(a.getPackageName())) { + if (filter.handlesWebUris(true)) { + handlesWebUris = true; + } + if (needsVerification && filter.needsVerification()) { if (DEBUG_DOMAIN_VERIFICATION) { - Slog.d(TAG, - "Intent filter needs verification, so processing all filters"); + Slog.d(TAG, "autoVerify requested, processing all filters"); } - needToVerify = true; + needToRunVerify = true; + // It's safe to break out here because filter.needsVerification() + // can only be true if filter.handlesWebUris(true) returned true, so + // we've already noted that. break; } } } - if (needToVerify) { - final boolean needsVerification = needsNetworkVerificationLPr(packageName); + // Compare the new set of recognized hosts if the app is either requesting + // autoVerify or has previously used autoVerify but no longer does. + if (needToRunVerify || previouslyVerified) { final int verificationId = mIntentFilterVerificationToken++; for (ParsedActivity a : activities) { for (ParsedIntentInfo filter : a.getIntents()) { // Run verification against hosts mentioned in any web-nav intent filter, // even if the filter matches non-web schemes as well - if (needsVerification && filter.handlesWebUris(false)) { + if (filter.handlesWebUris(false /*onlyWebSchemes*/)) { if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG, "Verification needed for IntentFilter:" + filter.toString()); mIntentFilterVerifier.addOneIntentFilterVerification( verifierUid, userId, verificationId, filter, packageName); + domains.addAll(filter.getHostsList()); count++; } } } } + + if (DEBUG_DOMAIN_VERIFICATION) { + Slog.i(TAG, " Update published hosts: " + domains.toString()); + } + + // If we've previously verified this same host set (or a subset), we can trust that + // a current ALWAYS policy is still applicable. If this is the case, we're done. + // (If we aren't in ALWAYS, we want to reverify to allow for apps that had failing + // hosts in their intent filters, then pushed a new apk that removed them and now + // passes.) + // + // Cases: + // + still autoVerify (needToRunVerify): + // - preserve current state if all of: unexpanded, in always + // - otherwise rerun as usual (fall through) + // + no longer autoVerify (alreadyVerified && !needToRunVerify) + // - wipe verification history always + // - preserve current state if all of: unexpanded, in always + hostSetExpanded = !previouslyVerified + || (ivi != null && !ivi.getDomains().containsAll(domains)); + final int currentPolicy = + mSettings.getIntentFilterVerificationStatusLPr(packageName, userId); + final boolean keepCurState = !hostSetExpanded + && currentPolicy == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS; + + if (needToRunVerify && keepCurState) { + if (DEBUG_DOMAIN_VERIFICATION) { + Slog.i(TAG, "Host set not expanding + ALWAYS -> no need to reverify"); + } + ivi.setDomains(domains); + scheduleWriteSettingsLocked(); + return; + } else if (previouslyVerified && !needToRunVerify) { + // Prior autoVerify state but not requesting it now. Clear autoVerify history, + // and preserve the always policy iff the host set is not expanding. + clearIntentFilterVerificationsLPw(packageName, userId, !keepCurState); + return; + } } - if (count > 0) { + if (needToRunVerify && count > 0) { + // app requested autoVerify and has at least one matching intent filter if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG, "Starting " + count + " IntentFilter verification" + (count > 1 ? "s" : "") + " for userId:" + userId); mIntentFilterVerifier.startVerifications(userId); } else { if (DEBUG_DOMAIN_VERIFICATION) { - Slog.d(TAG, "No filters or not all autoVerify for " + packageName); + Slog.d(TAG, "No web filters or no new host policy for " + packageName); } } } @@ -18402,7 +18451,7 @@ public class PackageManagerService extends IPackageManager.Stub if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) { final SparseBooleanArray changedUsers = new SparseBooleanArray(); synchronized (mLock) { - clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL); + clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL, true); clearDefaultBrowserIfNeeded(packageName); mSettings.mKeySetManagerService.removeAppKeySetDataLPw(packageName); removedAppId = mSettings.removePackageLPw(packageName); @@ -19495,13 +19544,14 @@ public class PackageManagerService extends IPackageManager.Stub final int packageCount = mPackages.size(); for (int i = 0; i < packageCount; i++) { AndroidPackage pkg = mPackages.valueAt(i); - clearIntentFilterVerificationsLPw(pkg.getPackageName(), userId); + clearIntentFilterVerificationsLPw(pkg.getPackageName(), userId, true); } } /** This method takes a specific user id as well as UserHandle.USER_ALL. */ @GuardedBy("mLock") - void clearIntentFilterVerificationsLPw(String packageName, int userId) { + void clearIntentFilterVerificationsLPw(String packageName, int userId, + boolean alsoResetStatus) { if (userId == UserHandle.USER_ALL) { if (mSettings.removeIntentFilterVerificationLPw(packageName, mUserManager.getUserIds())) { @@ -19510,7 +19560,8 @@ public class PackageManagerService extends IPackageManager.Stub } } } else { - if (mSettings.removeIntentFilterVerificationLPw(packageName, userId)) { + if (mSettings.removeIntentFilterVerificationLPw(packageName, userId, + alsoResetStatus)) { scheduleWritePackageRestrictionsLocked(userId); } } @@ -24182,6 +24233,25 @@ public class PackageManagerService extends IPackageManager.Stub } @Override + public void pruneCachedApksInApex(@NonNull List<PackageInfo> apexPackages) { + if (mCacheDir == null) { + return; + } + + final PackageCacher cacher = new PackageCacher(mCacheDir); + synchronized (mLock) { + for (int i = 0, size = apexPackages.size(); i < size; i++) { + final List<String> apkNames = + mApexManager.getApksInApex(apexPackages.get(i).packageName); + for (int j = 0, apksInApex = apkNames.size(); j < apksInApex; j++) { + final AndroidPackage pkg = getPackage(apkNames.get(j)); + cacher.cleanCachedResult(new File(pkg.getCodePath())); + } + } + } + } + + @Override public String getSetupWizardPackageName() { return mSetupWizardPackage; } @@ -24651,6 +24721,14 @@ public class PackageManagerService extends IPackageManager.Stub return packageName.equals( PackageManagerService.this.ensureSystemPackageName(packageName)); } + + @Override + public void clearBlockUninstallForUser(@UserIdInt int userId) { + synchronized (mLock) { + mSettings.clearBlockUninstallLPw(userId); + mSettings.writePackageRestrictionsLPr(userId); + } + } } @GuardedBy("mLock") diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 53c057a58a15..ddeab29c5b78 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -1282,7 +1282,8 @@ public final class Settings { return result; } - boolean removeIntentFilterVerificationLPw(String packageName, int userId) { + boolean removeIntentFilterVerificationLPw(String packageName, int userId, + boolean alsoResetStatus) { PackageSetting ps = mPackages.get(packageName); if (ps == null) { if (DEBUG_DOMAIN_VERIFICATION) { @@ -1290,14 +1291,17 @@ public final class Settings { } return false; } - ps.clearDomainVerificationStatusForUser(userId); + if (alsoResetStatus) { + ps.clearDomainVerificationStatusForUser(userId); + } + ps.setIntentFilterVerificationInfo(null); return true; } boolean removeIntentFilterVerificationLPw(String packageName, int[] userIds) { boolean result = false; for (int userId : userIds) { - result |= removeIntentFilterVerificationLPw(packageName, userId); + result |= removeIntentFilterVerificationLPw(packageName, userId, true); } return result; } @@ -1829,6 +1833,10 @@ public final class Settings { } } + void clearBlockUninstallLPw(int userId) { + mBlockUninstallPackages.remove(userId); + } + boolean getBlockUninstallLPr(int userId, String packageName) { ArraySet<String> packages = mBlockUninstallPackages.get(userId); if (packages == null) { diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index d1d98775c4b6..1c1e64d70bbd 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -1226,8 +1226,9 @@ public class StagingManager { // APEX checks. For single-package sessions, check if they contain an APEX. For // multi-package sessions, find all the child sessions that contain an APEX. if (hasApex) { + final List<PackageInfo> apexPackages; try { - final List<PackageInfo> apexPackages = submitSessionToApexService(session); + apexPackages = submitSessionToApexService(session); for (int i = 0, size = apexPackages.size(); i < size; i++) { validateApexSignature(apexPackages.get(i)); } @@ -1235,6 +1236,10 @@ public class StagingManager { session.setStagedSessionFailed(e.error, e.getMessage()); return; } + + final PackageManagerInternal packageManagerInternal = + LocalServices.getService(PackageManagerInternal.class); + packageManagerInternal.pruneCachedApksInApex(apexPackages); } notifyPreRebootVerification_Apex_Complete(session.sessionId); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index fc70af4e7bd4..4561d2e7a8b2 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -104,6 +104,7 @@ import com.android.internal.os.BackgroundThread; import com.android.internal.util.DumpUtils; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; import com.android.internal.util.XmlUtils; import com.android.internal.widget.LockPatternUtils; @@ -3153,13 +3154,17 @@ public class UserManagerService extends IUserManager.Stub { /** * Removes the app restrictions file for a specific package and user id, if it exists. + * + * @return whether there were any restrictions. */ - private static void cleanAppRestrictionsForPackageLAr(String pkg, @UserIdInt int userId) { - File dir = Environment.getUserSystemDirectory(userId); - File resFile = new File(dir, packageToRestrictionsFileName(pkg)); + private static boolean cleanAppRestrictionsForPackageLAr(String pkg, @UserIdInt int userId) { + final File dir = Environment.getUserSystemDirectory(userId); + final File resFile = new File(dir, packageToRestrictionsFileName(pkg)); if (resFile.exists()) { resFile.delete(); + return true; } + return false; } /** @@ -4003,17 +4008,24 @@ public class UserManagerService extends IUserManager.Stub { if (restrictions != null) { restrictions.setDefusable(true); } + final boolean changed; synchronized (mAppRestrictionsLock) { if (restrictions == null || restrictions.isEmpty()) { - cleanAppRestrictionsForPackageLAr(packageName, userId); + changed = cleanAppRestrictionsForPackageLAr(packageName, userId); } else { // Write the restrictions to XML writeApplicationRestrictionsLAr(packageName, restrictions, userId); + // TODO(b/154323615): avoid unnecessary broadcast when there is no change. + changed = true; } } + if (!changed) { + return; + } + // Notify package of changes via an intent - only sent to explicitly registered receivers. - Intent changeIntent = new Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED); + final Intent changeIntent = new Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED); changeIntent.setPackage(packageName); changeIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); mContext.sendBroadcastAsUser(changeIntent, UserHandle.of(userId)); @@ -4505,6 +4517,8 @@ public class UserManagerService extends IUserManager.Stub { switch(cmd) { case "list": return runList(pw, shell); + case "list-missing-system-packages": + return runListMissingSystemPackages(pw, shell); default: return shell.handleDefaultCommands(cmd); } @@ -4571,6 +4585,30 @@ public class UserManagerService extends IUserManager.Stub { } } + private int runListMissingSystemPackages(PrintWriter pw, Shell shell) { + boolean verbose = false; + boolean force = false; + String opt; + while ((opt = shell.getNextOption()) != null) { + switch (opt) { + case "-v": + verbose = true; + break; + case "--force": + force = true; + break; + default: + pw.println("Invalid option: " + opt); + return -1; + } + } + + try (IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ")) { + mSystemPackageInstaller.dumpMissingSystemPackages(ipw, force, verbose); + } + return 0; + } + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return; @@ -5143,6 +5181,9 @@ public class UserManagerService extends IUserManager.Stub { pw.println(""); pw.println(" list [-v] [-all]"); pw.println(" Prints all users on the system."); + pw.println(" list-missing-system-packages [-v] [--force]"); + pw.println(" Prints all system packages that were not explicitly configured to be " + + "installed."); } } diff --git a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java index 85c2306f6d89..cd1087f5fcd7 100644 --- a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java +++ b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java @@ -22,15 +22,16 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; -import android.content.pm.PackageParser; import android.content.res.Resources; import android.os.SystemProperties; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; import com.android.server.LocalServices; import com.android.server.SystemConfig; import com.android.server.pm.parsing.pkg.AndroidPackage; @@ -38,7 +39,9 @@ import com.android.server.pm.parsing.pkg.AndroidPackage; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Set; @@ -263,37 +266,85 @@ class UserSystemPackageInstaller { if (!isLogMode(mode) && !isEnforceMode(mode)) { return; } - Slog.v(TAG, "Checking that all system packages are whitelisted."); + final List<Pair<Boolean, String>> warnings = checkSystemPackagesWhitelistWarnings(mode); + final int size = warnings.size(); + if (size == 0) { + Slog.v(TAG, "checkWhitelistedSystemPackages(mode=" + mode + "): no warnings"); + return; + } + + if (isImplicitWhitelistMode(mode) && !isLogMode(mode)) { + // Only shows whether all whitelisted packages are indeed on the system. + for (int i = 0; i < size; i++) { + final Pair<Boolean, String> pair = warnings.get(i); + final boolean isSevere = pair.first; + if (!isSevere) { + final String msg = pair.second; + Slog.w(TAG, msg); + } + } + return; + } + + Slog.v(TAG, "checkWhitelistedSystemPackages(mode=" + mode + "): " + size + " warnings"); + boolean doWtf = !isImplicitWhitelistMode(mode); + for (int i = 0; i < size; i++) { + final Pair<Boolean, String> pair = warnings.get(i); + final boolean isSevere = pair.first; + final String msg = pair.second; + if (isSevere) { + if (doWtf) { + Slog.wtf(TAG, msg); + } else { + Slog.e(TAG, msg); + } + } else { + Slog.w(TAG, msg); + } + } + } + + // TODO: method below was created to refactor the one-time logging logic so it can be used on + // dump / cmd as well. It could to be further refactored (for example, creating a new + // structure for the warnings so it doesn't need a Pair). + /** + * Gets warnings for system user whitelisting. + * + * @return list of warnings, where {@code Pair.first} is the severity ({@code true} for WTF, + * {@code false} for WARN) and {@code Pair.second} the message. + */ + @NonNull + private List<Pair<Boolean, String>> checkSystemPackagesWhitelistWarnings( + @PackageWhitelistMode int mode) { final Set<String> allWhitelistedPackages = getWhitelistedSystemPackages(); - PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); + final List<Pair<Boolean, String>> warnings = new ArrayList<>(); + final PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); // Check whether all whitelisted packages are indeed on the system. + final String notPresentFmt = "%s is whitelisted but not present."; + final String notSystemFmt = "%s is whitelisted and present but not a system package."; for (String pkgName : allWhitelistedPackages) { - AndroidPackage pkg = pmInt.getPackage(pkgName); + final AndroidPackage pkg = pmInt.getPackage(pkgName); if (pkg == null) { - Slog.w(TAG, pkgName + " is whitelisted but not present."); + warnings.add(new Pair<>(false, String.format(notPresentFmt, pkgName))); } else if (!pkg.isSystem()) { - Slog.w(TAG, pkgName + " is whitelisted and present but not a system package."); + warnings.add(new Pair<>(false, String.format(notSystemFmt, pkgName))); } } // Check whether all system packages are indeed whitelisted. - if (isImplicitWhitelistMode(mode) && !isLogMode(mode)) { - return; - } - final boolean doWtf = isEnforceMode(mode); + final String logMessageFmt = "System package %s is not whitelisted using " + + "'install-in-user-type' in SystemConfig for any user types!"; + final boolean isSevere = isEnforceMode(mode); pmInt.forEachPackage(pkg -> { - if (pkg.isSystem() && !allWhitelistedPackages.contains(pkg.getManifestPackageName())) { - final String msg = "System package " + pkg.getManifestPackageName() - + " is not whitelisted using 'install-in-user-type' in SystemConfig " - + "for any user types!"; - if (doWtf) { - Slog.wtf(TAG, msg); - } else { - Slog.e(TAG, msg); - } + if (!pkg.isSystem()) return; + final String pkgName = pkg.getManifestPackageName(); + if (!allWhitelistedPackages.contains(pkgName)) { + warnings.add(new Pair<>(isSevere, String.format(logMessageFmt, pkgName))); } }); + + return warnings; } /** Whether to only install system packages in new users for which they are whitelisted. */ @@ -602,32 +653,45 @@ class UserSystemPackageInstaller { } void dump(PrintWriter pw) { - final String prefix = " "; - final String prefix2 = prefix + prefix; + try (IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ")) { + dumpIndented(ipw); + } + } + + private void dumpIndented(IndentingPrintWriter pw) { final int mode = getWhitelistMode(); pw.println("Whitelisted packages per user type"); - pw.print(prefix); pw.print("Mode: "); + + pw.increaseIndent(); + pw.print("Mode: "); pw.print(mode); pw.print(isEnforceMode(mode) ? " (enforced)" : ""); pw.print(isLogMode(mode) ? " (logged)" : ""); pw.print(isImplicitWhitelistMode(mode) ? " (implicit)" : ""); pw.print(isIgnoreOtaMode(mode) ? " (ignore OTAs)" : ""); pw.println(); + pw.decreaseIndent(); - pw.print(prefix); pw.println("Legend"); + pw.increaseIndent(); + pw.println("Legend"); + pw.increaseIndent(); for (int idx = 0; idx < mUserTypes.length; idx++) { - pw.print(prefix2); pw.println(idx + " -> " + mUserTypes[idx]); + pw.println(idx + " -> " + mUserTypes[idx]); } + pw.decreaseIndent(); pw.decreaseIndent(); + pw.increaseIndent(); final int size = mWhitelistedPackagesForUserTypes.size(); if (size == 0) { - pw.print(prefix); pw.println("No packages"); + pw.println("No packages"); + pw.decreaseIndent(); return; } - pw.print(prefix); pw.print(size); pw.println(" packages:"); + pw.print(size); pw.println(" packages:"); + pw.increaseIndent(); for (int pkgIdx = 0; pkgIdx < size; pkgIdx++) { final String pkgName = mWhitelistedPackagesForUserTypes.keyAt(pkgIdx); - pw.print(prefix2); pw.print(pkgName); pw.print(": "); + pw.print(pkgName); pw.print(": "); final long userTypesBitSet = mWhitelistedPackagesForUserTypes.valueAt(pkgIdx); for (int idx = 0; idx < mUserTypes.length; idx++) { if ((userTypesBitSet & (1 << idx)) != 0) { @@ -636,5 +700,40 @@ class UserSystemPackageInstaller { } pw.println(); } + pw.decreaseIndent(); pw.decreaseIndent(); + + pw.increaseIndent(); + dumpMissingSystemPackages(pw, /* force= */ true, /* verbose= */ true); + pw.decreaseIndent(); + } + + void dumpMissingSystemPackages(IndentingPrintWriter pw, boolean force, boolean verbose) { + final int mode = getWhitelistMode(); + final boolean show = force || (isEnforceMode(mode) && !isImplicitWhitelistMode(mode)); + if (!show) return; + + final List<Pair<Boolean, String>> warnings = checkSystemPackagesWhitelistWarnings(mode); + final int size = warnings.size(); + + if (size == 0) { + if (verbose) { + pw.println("All system packages are accounted for"); + } + return; + } + + if (verbose) { + pw.print(size); pw.println(" warnings for system user:"); + pw.increaseIndent(); + } + for (int i = 0; i < size; i++) { + final Pair<Boolean, String> pair = warnings.get(i); + final String lvl = pair.first ? "WTF" : "WARN"; + final String msg = pair.second; + pw.print(lvl); pw.print(": "); pw.println(msg); + } + if (verbose) { + pw.decreaseIndent(); + } } } diff --git a/services/core/java/com/android/server/pm/parsing/PackageCacher.java b/services/core/java/com/android/server/pm/parsing/PackageCacher.java index e5e1b0b20955..99c6dd1f0312 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageCacher.java +++ b/services/core/java/com/android/server/pm/parsing/PackageCacher.java @@ -18,6 +18,7 @@ package com.android.server.pm.parsing; import android.annotation.NonNull; import android.content.pm.PackageParserCacheHelper; +import android.os.FileUtils; import android.os.Parcel; import android.system.ErrnoException; import android.system.Os; @@ -197,4 +198,18 @@ public class PackageCacher { Slog.w(TAG, "Error saving package cache.", e); } } + + /** + * Delete the cache files for the given {@code packageFile}. + */ + public void cleanCachedResult(@NonNull File packageFile) { + final String packageName = packageFile.getName(); + final File[] files = FileUtils.listFilesOrEmpty(mCacheDir, + (dir, name) -> name.startsWith(packageName)); + for (File file : files) { + if (!file.delete()) { + Slog.e(TAG, "Unable to clean cache file: " + file); + } + } + } } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 4624e9ea0209..2f84a99774f4 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -62,6 +62,8 @@ import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER; import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; import static android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION; import static android.view.WindowManagerGlobal.ADD_OKAY; @@ -1328,6 +1330,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mScreenshotChordVolumeDownKeyConsumed = true; cancelPendingPowerKeyAction(); mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN); + mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_CHORD); mHandler.postDelayed(mScreenshotRunnable, getScreenshotChordLongPressDelay()); } } @@ -1411,14 +1414,19 @@ public class PhoneWindowManager implements WindowManagerPolicy { private class ScreenshotRunnable implements Runnable { private int mScreenshotType = TAKE_SCREENSHOT_FULLSCREEN; + private int mScreenshotSource = SCREENSHOT_KEY_OTHER; public void setScreenshotType(int screenshotType) { mScreenshotType = screenshotType; } + public void setScreenshotSource(int screenshotSource) { + mScreenshotSource = screenshotSource; + } + @Override public void run() { - mDefaultDisplayPolicy.takeScreenshot(mScreenshotType); + mDefaultDisplayPolicy.takeScreenshot(mScreenshotType, mScreenshotSource); } } @@ -2693,6 +2701,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { int type = event.isShiftPressed() ? TAKE_SCREENSHOT_SELECTED_REGION : TAKE_SCREENSHOT_FULLSCREEN; mScreenshotRunnable.setScreenshotType(type); + mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_OTHER); mHandler.post(mScreenshotRunnable); return -1; } @@ -2709,6 +2718,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } else if (keyCode == KeyEvent.KEYCODE_SYSRQ) { if (down && repeatCount == 0) { mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN); + mScreenshotRunnable.setScreenshotSource(SCREENSHOT_KEY_OTHER); mHandler.post(mScreenshotRunnable); } return -1; diff --git a/services/core/java/com/android/server/power/AttentionDetector.java b/services/core/java/com/android/server/power/AttentionDetector.java index 1ab6adee2320..b69c45070487 100644 --- a/services/core/java/com/android/server/power/AttentionDetector.java +++ b/services/core/java/com/android/server/power/AttentionDetector.java @@ -17,7 +17,6 @@ package com.android.server.power; import static android.provider.DeviceConfig.NAMESPACE_ATTENTION_MANAGER_SERVICE; -import static android.provider.Settings.Secure.ADAPTIVE_SLEEP; import android.Manifest; import android.app.ActivityManager; @@ -192,9 +191,6 @@ public class AttentionDetector { } if (!isAttentionServiceSupported() || !serviceHasSufficientPermissions()) { - // Turns off adaptive sleep in settings for all users if attention service is not - // available. The setting itself should also be grayed out in this case. - Settings.Secure.putInt(mContentResolver, ADAPTIVE_SLEEP, 0); return nextScreenDimming; } diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java index 059861b65e20..701197e690fc 100644 --- a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java @@ -214,7 +214,7 @@ public class BatterySaverPolicy extends ContentObserver { * required adjustments. */ @GuardedBy("mLock") - private Policy mEffectivePolicy = OFF_POLICY; + private Policy mEffectivePolicyRaw = OFF_POLICY; @IntDef(prefix = {"POLICY_LEVEL_"}, value = { POLICY_LEVEL_OFF, @@ -228,12 +228,8 @@ public class BatterySaverPolicy extends ContentObserver { static final int POLICY_LEVEL_ADAPTIVE = 1; static final int POLICY_LEVEL_FULL = 2; - /** - * Do not access directly; always use {@link #setPolicyLevel} - * and {@link #getPolicyLevelLocked} - */ @GuardedBy("mLock") - private int mPolicyLevelRaw = POLICY_LEVEL_OFF; + private int mPolicyLevel = POLICY_LEVEL_OFF; private final Context mContext; private final ContentResolver mContentResolver; @@ -338,7 +334,7 @@ public class BatterySaverPolicy extends ContentObserver { private void maybeNotifyListenersOfPolicyChange() { final BatterySaverPolicyListener[] listeners; synchronized (mLock) { - if (getPolicyLevelLocked() == POLICY_LEVEL_OFF) { + if (mPolicyLevel == POLICY_LEVEL_OFF) { // Current policy is OFF, so there's no change to notify listeners of. return; } @@ -428,14 +424,14 @@ public class BatterySaverPolicy extends ContentObserver { boolean changed = false; Policy newFullPolicy = Policy.fromSettings(setting, deviceSpecificSetting, DEFAULT_FULL_POLICY); - if (getPolicyLevelLocked() == POLICY_LEVEL_FULL && !mFullPolicy.equals(newFullPolicy)) { + if (mPolicyLevel == POLICY_LEVEL_FULL && !mFullPolicy.equals(newFullPolicy)) { changed = true; } mFullPolicy = newFullPolicy; mDefaultAdaptivePolicy = Policy.fromSettings(adaptiveSetting, adaptiveDeviceSpecificSetting, DEFAULT_ADAPTIVE_POLICY); - if (getPolicyLevelLocked() == POLICY_LEVEL_ADAPTIVE + if (mPolicyLevel == POLICY_LEVEL_ADAPTIVE && !mAdaptivePolicy.equals(mDefaultAdaptivePolicy)) { changed = true; } @@ -451,8 +447,9 @@ public class BatterySaverPolicy extends ContentObserver { @GuardedBy("mLock") private void updatePolicyDependenciesLocked() { final Policy rawPolicy = getCurrentRawPolicyLocked(); - final int locationMode; + + invalidatePowerSaveModeCaches(); if (mCarModeEnabled && rawPolicy.locationMode != PowerManager.LOCATION_MODE_NO_CHANGE && rawPolicy.locationMode != PowerManager.LOCATION_MODE_FOREGROUND_ONLY) { @@ -461,7 +458,8 @@ public class BatterySaverPolicy extends ContentObserver { } else { locationMode = rawPolicy.locationMode; } - mEffectivePolicy = new Policy( + + mEffectivePolicyRaw = new Policy( rawPolicy.adjustBrightnessFactor, rawPolicy.advertiseIsEnabled, rawPolicy.deferFullBackup, @@ -489,24 +487,24 @@ public class BatterySaverPolicy extends ContentObserver { final StringBuilder sb = new StringBuilder(); - if (mEffectivePolicy.forceAllAppsStandby) sb.append("A"); - if (mEffectivePolicy.forceBackgroundCheck) sb.append("B"); + if (mEffectivePolicyRaw.forceAllAppsStandby) sb.append("A"); + if (mEffectivePolicyRaw.forceBackgroundCheck) sb.append("B"); - if (mEffectivePolicy.disableVibration) sb.append("v"); - if (mEffectivePolicy.disableAnimation) sb.append("a"); - if (mEffectivePolicy.disableSoundTrigger) sb.append("s"); - if (mEffectivePolicy.deferFullBackup) sb.append("F"); - if (mEffectivePolicy.deferKeyValueBackup) sb.append("K"); - if (mEffectivePolicy.enableFirewall) sb.append("f"); - if (mEffectivePolicy.enableDataSaver) sb.append("d"); - if (mEffectivePolicy.enableAdjustBrightness) sb.append("b"); + if (mEffectivePolicyRaw.disableVibration) sb.append("v"); + if (mEffectivePolicyRaw.disableAnimation) sb.append("a"); + if (mEffectivePolicyRaw.disableSoundTrigger) sb.append("s"); + if (mEffectivePolicyRaw.deferFullBackup) sb.append("F"); + if (mEffectivePolicyRaw.deferKeyValueBackup) sb.append("K"); + if (mEffectivePolicyRaw.enableFirewall) sb.append("f"); + if (mEffectivePolicyRaw.enableDataSaver) sb.append("d"); + if (mEffectivePolicyRaw.enableAdjustBrightness) sb.append("b"); - if (mEffectivePolicy.disableLaunchBoost) sb.append("l"); - if (mEffectivePolicy.disableOptionalSensors) sb.append("S"); - if (mEffectivePolicy.disableAod) sb.append("o"); - if (mEffectivePolicy.enableQuickDoze) sb.append("q"); + if (mEffectivePolicyRaw.disableLaunchBoost) sb.append("l"); + if (mEffectivePolicyRaw.disableOptionalSensors) sb.append("S"); + if (mEffectivePolicyRaw.disableAod) sb.append("o"); + if (mEffectivePolicyRaw.enableQuickDoze) sb.append("q"); - sb.append(mEffectivePolicy.locationMode); + sb.append(mEffectivePolicyRaw.locationMode); mEventLogKeys = sb.toString(); } @@ -969,14 +967,14 @@ public class BatterySaverPolicy extends ContentObserver { */ boolean setPolicyLevel(@PolicyLevel int level) { synchronized (mLock) { - if (getPolicyLevelLocked() == level) { + if (mPolicyLevel == level) { return false; } switch (level) { case POLICY_LEVEL_FULL: case POLICY_LEVEL_ADAPTIVE: case POLICY_LEVEL_OFF: - setPolicyLevelLocked(level); + mPolicyLevel = level; break; default: Slog.wtf(TAG, "setPolicyLevel invalid level given: " + level); @@ -998,7 +996,7 @@ public class BatterySaverPolicy extends ContentObserver { } mAdaptivePolicy = p; - if (getPolicyLevelLocked() == POLICY_LEVEL_ADAPTIVE) { + if (mPolicyLevel == POLICY_LEVEL_ADAPTIVE) { updatePolicyDependenciesLocked(); return true; } @@ -1011,11 +1009,11 @@ public class BatterySaverPolicy extends ContentObserver { } private Policy getCurrentPolicyLocked() { - return mEffectivePolicy; + return mEffectivePolicyRaw; } private Policy getCurrentRawPolicyLocked() { - switch (getPolicyLevelLocked()) { + switch (mPolicyLevel) { case POLICY_LEVEL_FULL: return mFullPolicy; case POLICY_LEVEL_ADAPTIVE: @@ -1077,12 +1075,12 @@ public class BatterySaverPolicy extends ContentObserver { pw.println(" mAccessibilityEnabled=" + mAccessibilityEnabled); pw.println(" mCarModeEnabled=" + mCarModeEnabled); - pw.println(" mPolicyLevel=" + getPolicyLevelLocked()); + pw.println(" mPolicyLevel=" + mPolicyLevel); dumpPolicyLocked(pw, " ", "full", mFullPolicy); dumpPolicyLocked(pw, " ", "default adaptive", mDefaultAdaptivePolicy); dumpPolicyLocked(pw, " ", "current adaptive", mAdaptivePolicy); - dumpPolicyLocked(pw, " ", "effective", mEffectivePolicy); + dumpPolicyLocked(pw, " ", "effective", mEffectivePolicyRaw); } } @@ -1170,20 +1168,4 @@ public class BatterySaverPolicy extends ContentObserver { } } } - - /** Non-blocking getter exists as a reminder not to modify cached fields directly */ - @GuardedBy("mLock") - private int getPolicyLevelLocked() { - return mPolicyLevelRaw; - } - - @GuardedBy("mLock") - private void setPolicyLevelLocked(int level) { - if (mPolicyLevelRaw == level) { - return; - } - // Under lock, invalidate before set ensures caches won't return stale values. - invalidatePowerSaveModeCaches(); - mPolicyLevelRaw = level; - } } diff --git a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java index 801d75b90a54..f6d46e24246c 100644 --- a/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java +++ b/services/core/java/com/android/server/rollback/RollbackPackageHealthObserver.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.content.pm.VersionedPackage; import android.content.rollback.PackageRollbackInfo; import android.content.rollback.RollbackInfo; @@ -41,10 +42,13 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.LocalServices; import com.android.server.PackageWatchdog; import com.android.server.PackageWatchdog.FailureReasons; import com.android.server.PackageWatchdog.PackageHealthObserver; import com.android.server.PackageWatchdog.PackageHealthObserverImpact; +import com.android.server.pm.ApexManager; +import com.android.server.pm.parsing.pkg.AndroidPackage; import java.io.BufferedReader; import java.io.File; @@ -71,6 +75,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve private final Context mContext; private final Handler mHandler; + private final ApexManager mApexManager; private final File mLastStagedRollbackIdsFile; // Staged rollback ids that have been committed but their session is not yet ready @GuardedBy("mPendingStagedRollbackIds") @@ -85,6 +90,7 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve dataDir.mkdirs(); mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids"); PackageWatchdog.getInstance(mContext).registerHealthObserver(this); + mApexManager = ApexManager.getInstance(); } @Override @@ -302,6 +308,18 @@ public final class RollbackPackageHealthObserver implements PackageHealthObserve * Returns true if the package name is the name of a module. */ private boolean isModule(String packageName) { + // Check if the package is an APK inside an APEX. If it is, use the parent APEX package when + // querying PackageManager. + PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + AndroidPackage apkPackage = pmi.getPackage(packageName); + if (apkPackage != null) { + String apexPackageName = mApexManager.getActiveApexPackageNameContainingPackage( + apkPackage); + if (apexPackageName != null) { + packageName = apexPackageName; + } + } + PackageManager pm = mContext.getPackageManager(); try { return pm.getModuleInfo(packageName, 0) != null; diff --git a/services/core/java/com/android/server/tv/TvRemoteServiceInput.java b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java index 8fe6da5e8dbe..390340a13e51 100644 --- a/services/core/java/com/android/server/tv/TvRemoteServiceInput.java +++ b/services/core/java/com/android/server/tv/TvRemoteServiceInput.java @@ -88,6 +88,47 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { } @Override + public void openGamepadBridge(IBinder token, String name) throws RemoteException { + if (DEBUG) { + Slog.d(TAG, String.format("openGamepadBridge(), token: %s, name: %s", token, name)); + } + + synchronized (mLock) { + if (mBridgeMap.containsKey(token)) { + if (DEBUG) { + Slog.d(TAG, "InputBridge already exists"); + } + } else { + final long idToken = Binder.clearCallingIdentity(); + try { + mBridgeMap.put(token, UinputBridge.openGamepad(token, name)); + token.linkToDeath(new IBinder.DeathRecipient() { + @Override + public void binderDied() { + closeInputBridge(token); + } + }, 0); + } catch (IOException e) { + Slog.e(TAG, "Cannot create device for " + name); + return; + } catch (RemoteException e) { + Slog.e(TAG, "Token is already dead"); + closeInputBridge(token); + return; + } finally { + Binder.restoreCallingIdentity(idToken); + } + } + } + + try { + mProvider.onInputBridgeConnected(token); + } catch (RemoteException e) { + Slog.e(TAG, "Failed remote call to onInputBridgeConnected"); + } + } + + @Override public void closeInputBridge(IBinder token) { if (DEBUG) { Slog.d(TAG, "closeInputBridge(), token: " + token); @@ -96,6 +137,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.remove(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -117,6 +159,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -145,6 +188,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -166,6 +210,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -188,6 +233,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -209,6 +255,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -230,6 +277,7 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { synchronized (mLock) { UinputBridge inputBridge = mBridgeMap.get(token); if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); return; } @@ -241,4 +289,67 @@ final class TvRemoteServiceInput extends ITvRemoteServiceInput.Stub { } } } + + @Override + public void sendGamepadKeyUp(IBinder token, int keyIndex) { + if (DEBUG_KEYS) { + Slog.d(TAG, String.format("sendGamepadKeyUp(), token: %s", token)); + } + synchronized (mLock) { + UinputBridge inputBridge = mBridgeMap.get(token); + if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); + return; + } + + final long idToken = Binder.clearCallingIdentity(); + try { + inputBridge.sendGamepadKey(token, keyIndex, false); + } finally { + Binder.restoreCallingIdentity(idToken); + } + } + } + + @Override + public void sendGamepadKeyDown(IBinder token, int keyCode) { + if (DEBUG_KEYS) { + Slog.d(TAG, String.format("sendGamepadKeyDown(), token: %s", token)); + } + synchronized (mLock) { + UinputBridge inputBridge = mBridgeMap.get(token); + if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); + return; + } + + final long idToken = Binder.clearCallingIdentity(); + try { + inputBridge.sendGamepadKey(token, keyCode, true); + } finally { + Binder.restoreCallingIdentity(idToken); + } + } + } + + @Override + public void sendGamepadAxisValue(IBinder token, int axis, float value) { + if (DEBUG_KEYS) { + Slog.d(TAG, String.format("sendGamepadAxisValue(), token: %s", token)); + } + synchronized (mLock) { + UinputBridge inputBridge = mBridgeMap.get(token); + if (inputBridge == null) { + Slog.w(TAG, String.format("Input bridge not found for token: %s", token)); + return; + } + + final long idToken = Binder.clearCallingIdentity(); + try { + inputBridge.sendGamepadAxisValue(token, axis, value); + } finally { + Binder.restoreCallingIdentity(idToken); + } + } + } } diff --git a/services/core/java/com/android/server/tv/UinputBridge.java b/services/core/java/com/android/server/tv/UinputBridge.java index a2fe5fcde8c2..1dc201d4ee6b 100644 --- a/services/core/java/com/android/server/tv/UinputBridge.java +++ b/services/core/java/com/android/server/tv/UinputBridge.java @@ -42,21 +42,27 @@ public final class UinputBridge { /** Opens a gamepad - will support gamepad key and axis sending */ private static native long nativeGamepadOpen(String name, String uniqueId); - /** Marks the specified key up/down for a gamepad */ - private static native void nativeSendGamepadKey(long ptr, int keyIndex, boolean down); + /** + * Marks the specified key up/down for a gamepad. + * + * @param keyCode - a code like BUTTON_MODE, BUTTON_A, BUTTON_B, ... + */ + private static native void nativeSendGamepadKey(long ptr, int keyCode, boolean down); /** - * Gamepads pre-define the following axes: - * - Left joystick X, axis == ABS_X == 0, range [0, 254] - * - Left joystick Y, axis == ABS_Y == 1, range [0, 254] - * - Right joystick X, axis == ABS_RX == 3, range [0, 254] - * - Right joystick Y, axis == ABS_RY == 4, range [0, 254] - * - Left trigger, axis == ABS_Z == 2, range [0, 254] - * - Right trigger, axis == ABS_RZ == 5, range [0, 254] - * - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1] - * - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1] + * Send an axis value. + * + * Available axes are: + * <li> Left joystick: AXIS_X, AXIS_Y + * <li> Right joystick: AXIS_Z, AXIS_RZ + * <li> Analog triggers: AXIS_LTRIGGER, AXIS_RTRIGGER + * <li> DPad: AXIS_HAT_X, AXIS_HAT_Y + * + * @param axis is a MotionEvent.AXIS_* value. + * @param value is a value between -1 and 1 (inclusive) + * */ - private static native void nativeSendGamepadAxisValue(long ptr, int axis, int value); + private static native void nativeSendGamepadAxisValue(long ptr, int axis, float value); public UinputBridge(IBinder token, String name, int width, int height, int maxPointers) throws IOException { @@ -163,26 +169,19 @@ public final class UinputBridge { * @param keyIndex - the index of the w3-spec key * @param down - is the key pressed ? */ - public void sendGamepadKey(IBinder token, int keyIndex, boolean down) { + public void sendGamepadKey(IBinder token, int keyCode, boolean down) { if (isTokenValid(token)) { - nativeSendGamepadKey(mPtr, keyIndex, down); + nativeSendGamepadKey(mPtr, keyCode, down); } } - /** Send a gamepad axis value. - * - Left joystick X, axis == ABS_X == 0, range [0, 254] - * - Left joystick Y, axis == ABS_Y == 1, range [0, 254] - * - Right joystick X, axis == ABS_RX == 3, range [0, 254] - * - Right joystick Y, axis == ABS_RY == 4, range [0, 254] - * - Left trigger, axis == ABS_Z == 2, range [0, 254] - * - Right trigger, axis == ABS_RZ == 5, range [0, 254] - * - DPad X, axis == ABS_HAT0X == 0x10, range [-1, 1] - * - DPad Y, axis == ABS_HAT0Y == 0x11, range [-1, 1] + /** + * Send a gamepad axis value. * - * @param axis is the axis index - * @param value is the value to set for that axis + * @param axis is the axis code (MotionEvent.AXIS_*) + * @param value is the value to set for that axis in [-1, 1] */ - public void sendGamepadAxisValue(IBinder token, int axis, int value) { + public void sendGamepadAxisValue(IBinder token, int axis, float value) { if (isTokenValid(token)) { nativeSendGamepadAxisValue(mPtr, axis, value); } diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/CasResource.java b/services/core/java/com/android/server/tv/tunerresourcemanager/CasResource.java new file mode 100644 index 000000000000..54ad1d268e56 --- /dev/null +++ b/services/core/java/com/android/server/tv/tunerresourcemanager/CasResource.java @@ -0,0 +1,152 @@ +/* + * Copyright 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.tv.tunerresourcemanager; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A Cas resource object used by the Tuner Resource Manager to record the cas + * information. + * + * @hide + */ +public final class CasResource { + + private final int mSystemId; + + private int mMaxSessionNum; + + private int mAvailableSessionNum; + + /** + * The owner clients' ids when part of the Cas is occupied. + */ + private Map<Integer, Integer> mOwnerClientIdsToSessionNum = new HashMap<>(); + + private CasResource(Builder builder) { + this.mSystemId = builder.mSystemId; + this.mMaxSessionNum = builder.mMaxSessionNum; + this.mAvailableSessionNum = builder.mMaxSessionNum; + } + + public int getSystemId() { + return mSystemId; + } + + public int getMaxSessionNum() { + return mMaxSessionNum; + } + + public int getUsedSessionNum() { + return (mMaxSessionNum - mAvailableSessionNum); + } + + public boolean isFullyUsed() { + return mAvailableSessionNum == 0; + } + + /** + * Update max session number. + * + * @param maxSessionNum the new max session num. + */ + public void updateMaxSessionNum(int maxSessionNum) { + mAvailableSessionNum = Math.max( + 0, mAvailableSessionNum + (maxSessionNum - mMaxSessionNum)); + mMaxSessionNum = maxSessionNum; + } + + /** + * Set an owner for the cas + * + * @param ownerId the client id of the owner. + */ + public void setOwner(int ownerId) { + int sessionNum = mOwnerClientIdsToSessionNum.get(ownerId) == null + ? 1 : (mOwnerClientIdsToSessionNum.get(ownerId) + 1); + mOwnerClientIdsToSessionNum.put(ownerId, sessionNum); + mAvailableSessionNum--; + } + + /** + * Remove an owner of the Cas. + * + * @param ownerId the removing client id of the owner. + */ + public void removeOwner(int ownerId) { + mAvailableSessionNum += mOwnerClientIdsToSessionNum.get(ownerId); + mOwnerClientIdsToSessionNum.remove(ownerId); + } + + public Set<Integer> getOwnerClientIds() { + return mOwnerClientIdsToSessionNum.keySet(); + } + + @Override + public String toString() { + return "CasResource[systemId=" + this.mSystemId + + ", isFullyUsed=" + (this.mAvailableSessionNum == 0) + + ", maxSessionNum=" + this.mMaxSessionNum + + ", ownerClients=" + ownersMapToString() + "]"; + } + + /** + * Builder class for {@link CasResource}. + */ + public static class Builder { + + private int mSystemId; + private int mMaxSessionNum; + + Builder(int systemId) { + this.mSystemId = systemId; + } + + /** + * Builder for {@link CasResource}. + * + * @param maxSessionNum the max session num the current Cas has. + */ + public Builder maxSessionNum(int maxSessionNum) { + this.mMaxSessionNum = maxSessionNum; + return this; + } + + /** + * Build a {@link CasResource}. + * + * @return {@link CasResource}. + */ + public CasResource build() { + CasResource cas = new CasResource(this); + return cas; + } + } + + private String ownersMapToString() { + StringBuilder string = new StringBuilder("{"); + for (int clienId : mOwnerClientIdsToSessionNum.keySet()) { + string.append(" clientId=") + .append(clienId) + .append(", owns session num=") + .append(mOwnerClientIdsToSessionNum.get(clienId)) + .append(","); + } + return string.append("}").toString(); + } +} diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java index 4cdc17292ac4..2b0fe8a2602b 100644 --- a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java +++ b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java @@ -27,6 +27,7 @@ import java.util.Set; public final class ClientProfile { public static final int INVALID_GROUP_ID = -1; + public static final int INVALID_RESOURCE_ID = -1; /** * Client id sent to the client when registering with @@ -56,7 +57,6 @@ public final class ClientProfile { * also lose their resources. */ private int mGroupId = INVALID_GROUP_ID; - /** * Optional nice value for TRM to reduce client’s priority. */ @@ -73,6 +73,11 @@ public final class ClientProfile { private Set<Integer> mUsingLnbIds = new HashSet<>(); /** + * List of the Cas system ids that are used by the current client. + */ + private int mUsingCasSystemId = INVALID_RESOURCE_ID; + + /** * Optional arbitrary priority value given by the client. * * <p>This value can override the default priorotiy calculated from @@ -172,11 +177,32 @@ public final class ClientProfile { } /** + * Set when the client starts to use a Cas system. + * + * @param casSystemId cas being used. + */ + public void useCas(int casSystemId) { + mUsingCasSystemId = casSystemId; + } + + public int getInUseCasSystemId() { + return mUsingCasSystemId; + } + + /** + * Called when the client released a Cas System. + */ + public void releaseCas() { + mUsingCasSystemId = INVALID_RESOURCE_ID; + } + + /** * Called to reclaim all the resources being used by the current client. */ public void reclaimAllResources() { mUsingFrontendIds.clear(); mUsingLnbIds.clear(); + mUsingCasSystemId = INVALID_RESOURCE_ID; } @Override diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java index 7231813dd949..2f70840cfc8b 100644 --- a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java +++ b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java @@ -65,6 +65,8 @@ public class TunerResourceManagerService extends SystemService { private Map<Integer, FrontendResource> mFrontendResources = new HashMap<>(); // Map of the current available lnb resources private Map<Integer, LnbResource> mLnbResources = new HashMap<>(); + // Map of the current available Cas resources + private Map<Integer, CasResource> mCasResources = new HashMap<>(); @GuardedBy("mLock") private Map<Integer, ResourcesReclaimListenerRecord> mListeners = new HashMap<>(); @@ -158,10 +160,8 @@ public class TunerResourceManagerService extends SystemService { @Override public void updateCasInfo(int casSystemId, int maxSessionNum) { enforceTrmAccessPermission("updateCasInfo"); - if (DEBUG) { - Slog.d(TAG, - "updateCasInfo(casSystemId=" + casSystemId - + ", maxSessionNum=" + maxSessionNum + ")"); + synchronized (mLock) { + updateCasInfoInternal(casSystemId, maxSessionNum); } } @@ -185,11 +185,11 @@ public class TunerResourceManagerService extends SystemService { throw new RemoteException("frontendHandle can't be null"); } synchronized (mLock) { - try { - return requestFrontendInternal(request, frontendHandle); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (!checkClientExists(request.getClientId())) { + throw new RemoteException("Request frontend from unregistered client:" + + request.getClientId()); } + return requestFrontendInternal(request, frontendHandle); } } @@ -211,32 +211,45 @@ public class TunerResourceManagerService extends SystemService { throw new RemoteException("demuxHandle can't be null"); } synchronized (mLock) { + if (!checkClientExists(request.getClientId())) { + throw new RemoteException("Request demux from unregistered client:" + + request.getClientId()); + } return requestDemuxInternal(request, demuxHandle); } } @Override public boolean requestDescrambler(@NonNull TunerDescramblerRequest request, - @NonNull int[] descrambleHandle) throws RemoteException { + @NonNull int[] descramblerHandle) throws RemoteException { enforceDescramblerAccessPermission("requestDescrambler"); enforceTrmAccessPermission("requestDescrambler"); - if (descrambleHandle == null) { - throw new RemoteException("descrambleHandle can't be null"); + if (descramblerHandle == null) { + throw new RemoteException("descramblerHandle can't be null"); } synchronized (mLock) { - return requestDescramblerInternal(request, descrambleHandle); + if (!checkClientExists(request.getClientId())) { + throw new RemoteException("Request descrambler from unregistered client:" + + request.getClientId()); + } + return requestDescramblerInternal(request, descramblerHandle); } } @Override - public boolean requestCasSession( - @NonNull CasSessionRequest request, @NonNull int[] sessionResourceHandle) { + public boolean requestCasSession(@NonNull CasSessionRequest request, + @NonNull int[] casSessionHandle) throws RemoteException { enforceTrmAccessPermission("requestCasSession"); - if (DEBUG) { - Slog.d(TAG, "requestCasSession(request=" + request + ")"); + if (casSessionHandle == null) { + throw new RemoteException("casSessionHandle can't be null"); + } + synchronized (mLock) { + if (!checkClientExists(request.getClientId())) { + throw new RemoteException("Request cas from unregistered client:" + + request.getClientId()); + } + return requestCasSessionInternal(request, casSessionHandle); } - - return true; } @Override @@ -248,11 +261,11 @@ public class TunerResourceManagerService extends SystemService { throw new RemoteException("lnbHandle can't be null"); } synchronized (mLock) { - try { - return requestLnbInternal(request, lnbHandle); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (!checkClientExists(request.getClientId())) { + throw new RemoteException("Request lnb from unregistered client:" + + request.getClientId()); } + return requestLnbInternal(request, lnbHandle); } } @@ -264,15 +277,20 @@ public class TunerResourceManagerService extends SystemService { frontendHandle)) { throw new RemoteException("frontendHandle can't be invalid"); } - int frontendId = getResourceIdFromHandle(frontendHandle); - FrontendResource fe = getFrontendResource(frontendId); - if (fe == null) { - throw new RemoteException("Releasing frontend does not exist."); - } - if (fe.getOwnerClientId() != clientId) { - throw new RemoteException("Client is not the current owner of the releasing fe."); - } synchronized (mLock) { + if (!checkClientExists(clientId)) { + throw new RemoteException("Release frontend from unregistered client:" + + clientId); + } + int frontendId = getResourceIdFromHandle(frontendHandle); + FrontendResource fe = getFrontendResource(frontendId); + if (fe == null) { + throw new RemoteException("Releasing frontend does not exist."); + } + if (fe.getOwnerClientId() != clientId) { + throw new RemoteException( + "Client is not the current owner of the releasing fe."); + } releaseFrontendInternal(fe); } } @@ -296,10 +314,26 @@ public class TunerResourceManagerService extends SystemService { } @Override - public void releaseCasSession(int sessionResourceId, int clientId) { + public void releaseCasSession(int casSessionHandle, int clientId) throws RemoteException { enforceTrmAccessPermission("releaseCasSession"); - if (DEBUG) { - Slog.d(TAG, "releaseCasSession(sessionResourceId=" + sessionResourceId + ")"); + if (!validateResourceHandle( + TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION, casSessionHandle)) { + throw new RemoteException("casSessionHandle can't be invalid"); + } + synchronized (mLock) { + if (!checkClientExists(clientId)) { + throw new RemoteException("Release cas from unregistered client:" + clientId); + } + int casSystemId = getClientProfile(clientId).getInUseCasSystemId(); + CasResource cas = getCasResource(casSystemId); + if (cas == null) { + throw new RemoteException("Releasing cas does not exist."); + } + if (!cas.getOwnerClientIds().contains(clientId)) { + throw new RemoteException( + "Client is not the current owner of the releasing cas."); + } + releaseCasSessionInternal(cas, clientId); } } @@ -310,6 +344,9 @@ public class TunerResourceManagerService extends SystemService { if (!validateResourceHandle(TunerResourceManager.TUNER_RESOURCE_TYPE_LNB, lnbHandle)) { throw new RemoteException("lnbHandle can't be invalid"); } + if (!checkClientExists(clientId)) { + throw new RemoteException("Release lnb from unregistered client:" + clientId); + } int lnbId = getResourceIdFromHandle(lnbHandle); LnbResource lnb = getLnbResource(lnbId); if (lnb == null) { @@ -465,17 +502,42 @@ public class TunerResourceManagerService extends SystemService { } @VisibleForTesting - protected boolean requestFrontendInternal(TunerFrontendRequest request, int[] frontendHandle) - throws RemoteException { + protected void updateCasInfoInternal(int casSystemId, int maxSessionNum) { + if (DEBUG) { + Slog.d(TAG, + "updateCasInfo(casSystemId=" + casSystemId + + ", maxSessionNum=" + maxSessionNum + ")"); + } + // If maxSessionNum is 0, removing the Cas Resource. + if (maxSessionNum == 0) { + removeCasResource(casSystemId); + return; + } + // If the Cas exists, updates the Cas Resource accordingly. + CasResource cas = getCasResource(casSystemId); + if (cas != null) { + if (cas.getUsedSessionNum() > maxSessionNum) { + // Sort and release the short number of Cas resources. + int releasingCasResourceNum = cas.getUsedSessionNum() - maxSessionNum; + releaseLowerPriorityClientCasResources(releasingCasResourceNum); + } + cas.updateMaxSessionNum(maxSessionNum); + return; + } + // Add the new Cas Resource. + cas = new CasResource.Builder(casSystemId) + .maxSessionNum(maxSessionNum) + .build(); + addCasResource(cas); + } + + @VisibleForTesting + protected boolean requestFrontendInternal(TunerFrontendRequest request, int[] frontendHandle) { if (DEBUG) { Slog.d(TAG, "requestFrontend(request=" + request + ")"); } frontendHandle[0] = TunerResourceManager.INVALID_RESOURCE_HANDLE; - if (!checkClientExists(request.getClientId())) { - Slog.e(TAG, "Request frontend from unregistered client:" + request.getClientId()); - return false; - } ClientProfile requestClient = getClientProfile(request.getClientId()); int grantingFrontendId = -1; int inUseLowestPriorityFrId = -1; @@ -496,7 +558,7 @@ public class TunerResourceManagerService extends SystemService { } else if (grantingFrontendId < 0) { // Record the frontend id with the lowest client priority among all the // in use frontends when no available frontend has been found. - int priority = getOwnerClientPriority(fr); + int priority = getOwnerClientPriority(fr.getOwnerClientId()); if (currentLowestPriority > priority) { inUseLowestPriorityFrId = fr.getId(); currentLowestPriority = priority; @@ -530,17 +592,12 @@ public class TunerResourceManagerService extends SystemService { } @VisibleForTesting - protected boolean requestLnbInternal(TunerLnbRequest request, int[] lnbHandle) - throws RemoteException { + protected boolean requestLnbInternal(TunerLnbRequest request, int[] lnbHandle) { if (DEBUG) { Slog.d(TAG, "requestLnb(request=" + request + ")"); } lnbHandle[0] = TunerResourceManager.INVALID_RESOURCE_HANDLE; - if (!checkClientExists(request.getClientId())) { - Slog.e(TAG, "Request lnb from unregistered client:" + request.getClientId()); - return false; - } ClientProfile requestClient = getClientProfile(request.getClientId()); int grantingLnbId = -1; int inUseLowestPriorityLnbId = -1; @@ -554,7 +611,7 @@ public class TunerResourceManagerService extends SystemService { } else { // Record the lnb id with the lowest client priority among all the // in use lnb when no available lnb has been found. - int priority = getOwnerClientPriority(lnb); + int priority = getOwnerClientPriority(lnb.getOwnerClientId()); if (currentLowestPriority > priority) { inUseLowestPriorityLnbId = lnb.getId(); currentLowestPriority = priority; @@ -588,7 +645,55 @@ public class TunerResourceManagerService extends SystemService { } @VisibleForTesting - void releaseFrontendInternal(FrontendResource fe) { + protected boolean requestCasSessionInternal(CasSessionRequest request, int[] casSessionHandle) { + if (DEBUG) { + Slog.d(TAG, "requestCasSession(request=" + request + ")"); + } + CasResource cas = getCasResource(request.getCasSystemId()); + // Unregistered Cas System is treated as having unlimited sessions. + if (cas == null) { + cas = new CasResource.Builder(request.getCasSystemId()) + .maxSessionNum(Integer.MAX_VALUE) + .build(); + addCasResource(cas); + } + casSessionHandle[0] = TunerResourceManager.INVALID_RESOURCE_HANDLE; + ClientProfile requestClient = getClientProfile(request.getClientId()); + int lowestPriorityOwnerId = -1; + // Priority max value is 1000 + int currentLowestPriority = MAX_CLIENT_PRIORITY + 1; + if (!cas.isFullyUsed()) { + casSessionHandle[0] = generateResourceHandle( + TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION, cas.getSystemId()); + updateCasClientMappingOnNewGrant(request.getCasSystemId(), request.getClientId()); + return true; + } + for (int ownerId : cas.getOwnerClientIds()) { + // Record the client id with lowest priority that is using the current Cas system. + int priority = getOwnerClientPriority(ownerId); + if (currentLowestPriority > priority) { + lowestPriorityOwnerId = ownerId; + currentLowestPriority = priority; + } + } + + // When all the Cas sessions are occupied, reclaim the lowest priority client if the + // request client has higher priority. + if (lowestPriorityOwnerId > -1 && (requestClient.getPriority() > currentLowestPriority)) { + if (!reclaimResource(lowestPriorityOwnerId, + TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION)) { + return false; + } + casSessionHandle[0] = generateResourceHandle( + TunerResourceManager.TUNER_RESOURCE_TYPE_CAS_SESSION, cas.getSystemId()); + updateCasClientMappingOnNewGrant(request.getCasSystemId(), request.getClientId()); + return true; + } + return false; + } + + @VisibleForTesting + protected void releaseFrontendInternal(FrontendResource fe) { if (DEBUG) { Slog.d(TAG, "releaseFrontend(id=" + fe.getId() + ")"); } @@ -596,7 +701,7 @@ public class TunerResourceManagerService extends SystemService { } @VisibleForTesting - void releaseLnbInternal(LnbResource lnb) { + protected void releaseLnbInternal(LnbResource lnb) { if (DEBUG) { Slog.d(TAG, "releaseLnb(lnbId=" + lnb.getId() + ")"); } @@ -604,7 +709,15 @@ public class TunerResourceManagerService extends SystemService { } @VisibleForTesting - boolean requestDemuxInternal(TunerDemuxRequest request, int[] demuxHandle) { + protected void releaseCasSessionInternal(CasResource cas, int ownerClientId) { + if (DEBUG) { + Slog.d(TAG, "releaseCasSession(sessionResourceId=" + cas.getSystemId() + ")"); + } + updateCasClientMappingOnRelease(cas, ownerClientId); + } + + @VisibleForTesting + protected boolean requestDemuxInternal(TunerDemuxRequest request, int[] demuxHandle) { if (DEBUG) { Slog.d(TAG, "requestDemux(request=" + request + ")"); } @@ -614,7 +727,8 @@ public class TunerResourceManagerService extends SystemService { } @VisibleForTesting - boolean requestDescramblerInternal(TunerDescramblerRequest request, int[] descramblerHandle) { + protected boolean requestDescramblerInternal( + TunerDescramblerRequest request, int[] descramblerHandle) { if (DEBUG) { Slog.d(TAG, "requestDescrambler(request=" + request + ")"); } @@ -742,14 +856,28 @@ public class TunerResourceManagerService extends SystemService { ownerProfile.releaseLnb(releasingLnb.getId()); } + private void updateCasClientMappingOnNewGrant(int grantingId, int ownerClientId) { + CasResource grantingCas = getCasResource(grantingId); + ClientProfile ownerProfile = getClientProfile(ownerClientId); + grantingCas.setOwner(ownerClientId); + ownerProfile.useCas(grantingId); + } + + private void updateCasClientMappingOnRelease( + @NonNull CasResource releasingCas, int ownerClientId) { + ClientProfile ownerProfile = getClientProfile(ownerClientId); + releasingCas.removeOwner(ownerClientId); + ownerProfile.releaseCas(); + } + /** * Get the owner client's priority from the resource id. * - * @param resource a in use tuner resource. + * @param clientId the owner client id. * @return the priority of the owner client of the resource. */ - private int getOwnerClientPriority(TunerResourceBasic resource) { - return getClientProfile(resource.getOwnerClientId()).getPriority(); + private int getOwnerClientPriority(int clientId) { + return getClientProfile(clientId).getPriority(); } @VisibleForTesting @@ -783,6 +911,9 @@ public class TunerResourceManagerService extends SystemService { private void removeFrontendResource(int removingId) { FrontendResource fe = getFrontendResource(removingId); + if (fe == null) { + return; + } if (fe.isInUse()) { releaseFrontendInternal(fe); } @@ -811,6 +942,9 @@ public class TunerResourceManagerService extends SystemService { private void removeLnbResource(int removingId) { LnbResource lnb = getLnbResource(removingId); + if (lnb == null) { + return; + } if (lnb.isInUse()) { releaseLnbInternal(lnb); } @@ -819,6 +953,39 @@ public class TunerResourceManagerService extends SystemService { @VisibleForTesting @Nullable + protected CasResource getCasResource(int systemId) { + return mCasResources.get(systemId); + } + + @VisibleForTesting + protected Map<Integer, CasResource> getCasResources() { + return mCasResources; + } + + private void addCasResource(CasResource newCas) { + // Update resource list and available id list + mCasResources.put(newCas.getSystemId(), newCas); + } + + private void removeCasResource(int removingId) { + CasResource cas = getCasResource(removingId); + if (cas == null) { + return; + } + for (int ownerId : cas.getOwnerClientIds()) { + getClientProfile(ownerId).releaseCas(); + } + mCasResources.remove(removingId); + } + + private void releaseLowerPriorityClientCasResources(int releasingCasResourceNum) { + // TODO: Sort with a treemap + + // select the first num client to release + } + + @VisibleForTesting + @Nullable protected ClientProfile getClientProfile(int clientId) { return mClientProfiles.get(clientId); } @@ -830,12 +997,7 @@ public class TunerResourceManagerService extends SystemService { } private void removeClientProfile(int clientId) { - for (int id : getClientProfile(clientId).getInUseFrontendIds()) { - getFrontendResource(id).removeOwner(); - for (int groupMemberId : getFrontendResource(id).getExclusiveGroupMemberFeIds()) { - getFrontendResource(groupMemberId).removeOwner(); - } - } + reclaimingResourcesFromClient(getClientProfile(clientId)); mClientProfiles.remove(clientId); mListeners.remove(clientId); } @@ -847,6 +1009,9 @@ public class TunerResourceManagerService extends SystemService { for (Integer lnbId : profile.getInUseLnbIds()) { getLnbResource(lnbId).removeOwner(); } + if (profile.getInUseCasSystemId() != ClientProfile.INVALID_RESOURCE_ID) { + getCasResource(profile.getInUseCasSystemId()).removeOwner(profile.getId()); + } profile.reclaimAllResources(); } diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 3f9f95cf8370..abccf99579b7 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -1245,6 +1245,16 @@ final class AccessibilityController { } } + for (int i = dc.mShellRoots.size() - 1; i >= 0; --i) { + final WindowInfo info = dc.mShellRoots.valueAt(i).getWindowInfo(); + if (info == null) { + continue; + } + info.layer = addedWindows.size(); + windows.add(info); + addedWindows.add(info.token); + } + // Remove child/parent references to windows that were not added. final int windowCount = windows.size(); for (int i = 0; i < windowCount; i++) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index e76eda06d2d3..2648c86d3c6a 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -4712,7 +4712,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A */ private boolean shouldBeResumed(ActivityRecord activeActivity) { return shouldMakeActive(activeActivity) && isFocusable() - && getRootTask().getVisibility(activeActivity) == STACK_VISIBILITY_VISIBLE + && getTask().getVisibility(activeActivity) == STACK_VISIBILITY_VISIBLE && canResumeByCompat(); } diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java index 0f574963eb34..ff43e77a5c47 100644 --- a/services/core/java/com/android/server/wm/ActivityStack.java +++ b/services/core/java/com/android/server/wm/ActivityStack.java @@ -18,11 +18,8 @@ package com.android.server.wm; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; -import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; -import static android.app.WindowConfiguration.PINNED_WINDOWING_MODE_ELEVATION_IN_DIP; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; @@ -142,13 +139,11 @@ import android.os.SystemClock; import android.os.Trace; import android.os.UserHandle; import android.service.voice.IVoiceInteractionSession; -import android.util.DisplayMetrics; import android.util.Log; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.Display; import android.view.DisplayInfo; -import android.view.SurfaceControl; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -157,7 +152,6 @@ import com.android.internal.os.logging.MetricsLoggerWrapper; import com.android.internal.util.function.pooled.PooledConsumer; import com.android.internal.util.function.pooled.PooledFunction; import com.android.internal.util.function.pooled.PooledLambda; -import com.android.internal.util.function.pooled.PooledPredicate; import com.android.server.Watchdog; import com.android.server.am.ActivityManagerService; import com.android.server.am.ActivityManagerService.ItemMatcher; @@ -276,11 +270,6 @@ class ActivityStack extends Task { Rect mPreAnimationBounds = new Rect(); - /** - * For {@link #prepareSurfaces}. - */ - private final Point mLastSurfaceSize = new Point(); - private final AnimatingActivityRegistry mAnimatingActivityRegistry = new AnimatingActivityRegistry(); @@ -606,10 +595,6 @@ class ActivityStack extends Task { super.onConfigurationChanged(newParentConfig); - // Only need to update surface size here since the super method will handle updating - // surface position. - updateSurfaceSize(getPendingTransaction()); - final TaskDisplayArea taskDisplayArea = getDisplayArea(); if (taskDisplayArea == null) { return; @@ -3262,61 +3247,14 @@ class ActivityStack extends Task { scheduleAnimation(); } - /** - * Calculate an amount by which to expand the stack bounds in each direction. - * Used to make room for shadows in the pinned windowing mode. - */ - int getStackOutset() { - // If we are drawing shadows on the task then don't outset the stack. - if (mWmService.mRenderShadowsInCompositor) { - return 0; - } - DisplayContent displayContent = getDisplayContent(); - if (inPinnedWindowingMode() && displayContent != null) { - final DisplayMetrics displayMetrics = displayContent.getDisplayMetrics(); - - // We multiply by two to match the client logic for converting view elevation - // to insets, as in {@link WindowManager.LayoutParams#setSurfaceInsets} - return (int) Math.ceil( - mWmService.dipToPixel(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP, displayMetrics) - * 2); - } - return 0; - } - @Override void getRelativePosition(Point outPos) { super.getRelativePosition(outPos); - final int outset = getStackOutset(); + final int outset = getTaskOutset(); outPos.x -= outset; outPos.y -= outset; } - private void updateSurfaceSize(SurfaceControl.Transaction transaction) { - if (mSurfaceControl == null) { - return; - } - - final Rect stackBounds = getBounds(); - int width = stackBounds.width(); - int height = stackBounds.height(); - - final int outset = getStackOutset(); - width += 2 * outset; - height += 2 * outset; - - if (width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) { - return; - } - transaction.setWindowCrop(mSurfaceControl, width, height); - mLastSurfaceSize.set(width, height); - } - - @VisibleForTesting - Point getLastSurfaceSize() { - return mLastSurfaceSize; - } - @Override void onDisplayChanged(DisplayContent dc) { super.onDisplayChanged(dc); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 736bc9f8bc22..93a757449564 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5400,14 +5400,11 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { final boolean inputMethodExists = !(config.keyboard == Configuration.KEYBOARD_NOKEYS && config.touchscreen == Configuration.TOUCHSCREEN_NOTOUCH && config.navigation == Configuration.NAVIGATION_NONAV); - int modeType = config.uiMode & Configuration.UI_MODE_TYPE_MASK; - final boolean uiModeSupportsDialogs = (modeType != Configuration.UI_MODE_TYPE_CAR - && !(modeType == Configuration.UI_MODE_TYPE_WATCH && Build.IS_USER) - && modeType != Configuration.UI_MODE_TYPE_TELEVISION - && modeType != Configuration.UI_MODE_TYPE_VR_HEADSET); final boolean hideDialogsSet = Settings.Global.getInt(mContext.getContentResolver(), HIDE_ERROR_DIALOGS, 0) != 0; - mShowDialogs = inputMethodExists && uiModeSupportsDialogs && !hideDialogsSet; + mShowDialogs = inputMethodExists + && ActivityTaskManager.currentUiModeSupportsErrorDialogs(mContext) + && !hideDialogsSet; } private void updateFontScaleIfNeeded(@UserIdInt int userId) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index a47cdc66fbd8..e26163247020 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -26,7 +26,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; -import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET; @@ -574,7 +573,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo /** Corner radius that windows should have in order to match the display. */ private final float mWindowCornerRadius; - private final SparseArray<ShellRoot> mShellRoots = new SparseArray<>(); + final SparseArray<ShellRoot> mShellRoots = new SparseArray<>(); RemoteInsetsControlTarget mRemoteInsetsControlTarget = null; private final IBinder.DeathRecipient mRemoteInsetsDeath = () -> { @@ -5450,6 +5449,46 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo return mWmService.mDisplayManagerInternal.getDisplayPosition(getDisplayId()); } + /** + * Locates the appropriate target window for scroll capture. The search progresses top to + * bottom. + * If {@code searchBehind} is non-null, the search will only consider windows behind this one. + * If a valid taskId is specified, the target window must belong to the given task. + * + * @param searchBehind a window used to filter the search to windows behind it, or null to begin + * the search at the top window of the display + * @param taskId specifies the id of a task the result must belong to or + * {@link android.app.ActivityTaskManager#INVALID_TASK_ID INVALID_TASK_ID} + * to match any window + * @return the located window or null if none could be found matching criteria + */ + @Nullable + WindowState findScrollCaptureTargetWindow(@Nullable WindowState searchBehind, int taskId) { + return getWindow(new Predicate<WindowState>() { + boolean behindTopWindow = (searchBehind == null); // optional filter + @Override + public boolean test(WindowState nextWindow) { + // Skip through all windows until we pass topWindow (if specified) + if (!behindTopWindow) { + if (nextWindow == searchBehind) { + behindTopWindow = true; + } + return false; /* continue */ + } + if (taskId != INVALID_TASK_ID) { + Task task = nextWindow.getTask(); + if (task == null || !task.isTaskId(taskId)) { + return false; /* continue */ + } + } + if (!nextWindow.canReceiveKeys()) { + return false; /* continue */ + } + return true; /* stop */ + } + }); + } + class RemoteInsetsControlTarget implements InsetsControlTarget { private final IDisplayWindowInsetsController mRemoteInsetsController; diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index e9d3d56ee283..8aace212d094 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -261,6 +261,8 @@ public class DisplayPolicy { @Px private int mRightGestureInset; + private boolean mNavButtonForcedVisible; + StatusBarManagerInternal getStatusBarManagerInternal() { synchronized (mServiceAcquireLock) { if (mStatusBarManagerInternal == null) { @@ -1046,12 +1048,14 @@ public class DisplayPolicy { // calculate inset. if (navigationBarPosition(displayFrames.mDisplayWidth, displayFrames.mDisplayHeight, - displayFrames.mRotation) == NAV_BAR_BOTTOM) { + displayFrames.mRotation) == NAV_BAR_BOTTOM + && !mNavButtonForcedVisible) { + sTmpRect.set(displayFrames.mUnrestricted); sTmpRect.intersectUnchecked(displayFrames.mDisplayCutoutSafe); inOutFrame.top = sTmpRect.bottom - getNavigationBarHeight(displayFrames.mRotation, - mDisplayContent.getConfiguration().uiMode); + mDisplayContent.getConfiguration().uiMode); } }, @@ -2810,6 +2814,8 @@ public class DisplayPolicy { mNavBarOpacityMode = res.getInteger(R.integer.config_navBarOpacityMode); mLeftGestureInset = mGestureNavigationSettingsObserver.getLeftSensitivity(res); mRightGestureInset = mGestureNavigationSettingsObserver.getRightSensitivity(res); + mNavButtonForcedVisible = + mGestureNavigationSettingsObserver.areNavigationButtonForcedVisible(); mNavigationBarLetsThroughTaps = res.getBoolean(R.bool.config_navBarTapThrough); mNavigationBarAlwaysShowOnSideGesture = res.getBoolean(R.bool.config_navBarAlwaysShowOnSideEdgeGesture); @@ -3783,13 +3789,14 @@ public class DisplayPolicy { * @param screenshotType The type of screenshot, for example either * {@link WindowManager#TAKE_SCREENSHOT_FULLSCREEN} or * {@link WindowManager#TAKE_SCREENSHOT_SELECTED_REGION} + * @param source Where the screenshot originated from (see WindowManager.ScreenshotSource) */ - public void takeScreenshot(int screenshotType) { + public void takeScreenshot(int screenshotType, int source) { if (mScreenshotHelper != null) { mScreenshotHelper.takeScreenshot(screenshotType, mStatusBar != null && mStatusBar.isVisibleLw(), mNavigationBar != null && mNavigationBar.isVisibleLw(), - mHandler, null /* completionConsumer */); + source, mHandler, null /* completionConsumer */); } } diff --git a/services/core/java/com/android/server/wm/DockedStackDividerController.java b/services/core/java/com/android/server/wm/DockedStackDividerController.java index 20738ed29470..803bec8941a8 100644 --- a/services/core/java/com/android/server/wm/DockedStackDividerController.java +++ b/services/core/java/com/android/server/wm/DockedStackDividerController.java @@ -45,6 +45,11 @@ public class DockedStackDividerController { void setTouchRegion(Rect touchRegion) { mTouchRegion.set(touchRegion); + // We need to report touchable region changes to accessibility. + if (mDisplayContent.mWmService.mAccessibilityController != null) { + mDisplayContent.mWmService.mAccessibilityController.onSomeWindowResizedOrMovedLocked( + mDisplayContent.getDisplayId()); + } } void getTouchRegion(Rect outRegion) { diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index 8b34b9b8dd8f..656dca531a22 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -487,7 +487,8 @@ final class InputMonitor { || w.cantReceiveTouchInput()) { if (w.mWinAnimator.hasSurface()) { mInputTransaction.setInputWindowInfo( - w.mWinAnimator.mSurfaceController.mSurfaceControl, mInvalidInputWindow); + w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), + mInvalidInputWindow); } // Skip this window because it cannot possibly receive input. return; @@ -560,7 +561,8 @@ final class InputMonitor { if (w.mWinAnimator.hasSurface()) { mInputTransaction.setInputWindowInfo( - w.mWinAnimator.mSurfaceController.mSurfaceControl, inputWindowHandle); + w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), + inputWindowHandle); } } } diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java index 5f33ea170923..86e081854597 100644 --- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java +++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java @@ -212,7 +212,7 @@ class ScreenRotationAnimation { final Surface surface = mService.mSurfaceFactory.get(); surface.copyFrom(mScreenshotLayer); SurfaceControl.ScreenshotGraphicBuffer gb = - mService.mDisplayManagerInternal.screenshot(displayId); + mService.mDisplayManagerInternal.systemScreenshot(displayId); if (gb != null) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "ScreenRotationAnimation#getMedianBorderLuma"); @@ -597,8 +597,8 @@ class ScreenRotationAnimation { return startAnimation(initializeBuilder() .setSurfaceControl(mScreenshotLayer) .setAnimationLeashParent(mDisplayContent.getOverlayLayer()) - .setWidth(mWidth) - .setHeight(mHeight) + .setWidth(mDisplayContent.getSurfaceWidth()) + .setHeight(mDisplayContent.getSurfaceHeight()) .build(), createWindowAnimationSpec(mRotateAlphaAnimation), this::onAnimationEnd); diff --git a/services/core/java/com/android/server/wm/ShellRoot.java b/services/core/java/com/android/server/wm/ShellRoot.java index 701feff8c6be..0b1760dc5a1c 100644 --- a/services/core/java/com/android/server/wm/ShellRoot.java +++ b/services/core/java/com/android/server/wm/ShellRoot.java @@ -23,12 +23,14 @@ import static com.android.server.wm.WindowManagerService.MAX_ANIMATION_DURATION; import android.annotation.NonNull; import android.graphics.Point; +import android.graphics.Rect; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; import android.view.DisplayInfo; import android.view.IWindow; import android.view.SurfaceControl; +import android.view.WindowInfo; import android.view.animation.Animation; /** @@ -102,5 +104,27 @@ public class ShellRoot { mToken.startAnimation(mToken.getPendingTransaction(), adapter, false /* hidden */, ANIMATION_TYPE_WINDOW_ANIMATION, null /* animationFinishedCallback */); } + + WindowInfo getWindowInfo() { + if (mToken.windowType != TYPE_DOCK_DIVIDER) { + return null; + } + if (!mDisplayContent.getDefaultTaskDisplayArea().isSplitScreenModeActivated()) { + return null; + } + WindowInfo windowInfo = WindowInfo.obtain(); + windowInfo.displayId = mToken.getDisplayArea().getDisplayContent().mDisplayId; + windowInfo.type = mToken.windowType; + windowInfo.layer = mToken.getWindowLayerFromType(); + windowInfo.token = mClient.asBinder(); + windowInfo.title = "Splitscreen Divider"; + windowInfo.focused = false; + windowInfo.inPictureInPicture = false; + windowInfo.hasFlagWatchOutsideTouch = false; + final Rect regionRect = new Rect(); + mDisplayContent.getDockedDividerController().getTouchRegion(regionRect); + windowInfo.regionInScreen.set(regionRect); + return windowInfo; + } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 66ca0ac85143..66e1b1758d85 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -120,6 +120,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.graphics.Point; import android.graphics.Rect; import android.os.Debug; import android.os.IBinder; @@ -213,7 +214,6 @@ class Task extends WindowContainer<WindowContainer> { static final int INVALID_MIN_SIZE = -1; private float mShadowRadius = 0; - private final Rect mLastSurfaceCrop = new Rect(); /** * The modes to control how the stack is moved to the front when calling {@link Task#reparent}. @@ -397,6 +397,7 @@ class Task extends WindowContainer<WindowContainer> { private Dimmer mDimmer = new Dimmer(this); private final Rect mTmpDimBoundsRect = new Rect(); + private final Point mLastSurfaceSize = new Point(); /** @see #setCanAffectSystemUiFlags */ private boolean mCanAffectSystemUiFlags = true; @@ -1943,6 +1944,10 @@ class Task extends WindowContainer<WindowContainer> { mTmpPrevBounds.set(getBounds()); final boolean wasInMultiWindowMode = inMultiWindowMode(); super.onConfigurationChanged(newParentConfig); + // Only need to update surface size here since the super method will handle updating + // surface position. + updateSurfaceSize(getPendingTransaction()); + if (wasInMultiWindowMode != inMultiWindowMode()) { mStackSupervisor.scheduleUpdateMultiWindowMode(this); } @@ -1995,6 +2000,57 @@ class Task extends WindowContainer<WindowContainer> { return (prevWinMode == WINDOWING_MODE_FREEFORM) != (newWinMode == WINDOWING_MODE_FREEFORM); } + void updateSurfaceSize(SurfaceControl.Transaction transaction) { + if (mSurfaceControl == null || mCreatedByOrganizer) { + return; + } + + // Apply crop to root tasks only and clear the crops of the descendant tasks. + int width = 0; + int height = 0; + if (isRootTask()) { + final Rect taskBounds = getBounds(); + width = taskBounds.width(); + height = taskBounds.height(); + + final int outset = getTaskOutset(); + width += 2 * outset; + height += 2 * outset; + } + if (width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) { + return; + } + transaction.setWindowCrop(mSurfaceControl, width, height); + mLastSurfaceSize.set(width, height); + } + + /** + * Calculate an amount by which to expand the task bounds in each direction. + * Used to make room for shadows in the pinned windowing mode. + */ + int getTaskOutset() { + // If we are drawing shadows on the task then don't outset the stack. + if (mWmService.mRenderShadowsInCompositor) { + return 0; + } + DisplayContent displayContent = getDisplayContent(); + if (inPinnedWindowingMode() && displayContent != null) { + final DisplayMetrics displayMetrics = displayContent.getDisplayMetrics(); + + // We multiply by two to match the client logic for converting view elevation + // to insets, as in {@link WindowManager.LayoutParams#setSurfaceInsets} + return (int) Math.ceil( + mWmService.dipToPixel(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP, displayMetrics) + * 2); + } + return 0; + } + + @VisibleForTesting + Point getLastSurfaceSize() { + return mLastSurfaceSize; + } + @VisibleForTesting boolean isInChangeTransition() { return mSurfaceFreezer.hasLeash() || AppTransition.isChangeTransit(mTransit); @@ -2225,14 +2281,16 @@ class Task extends WindowContainer<WindowContainer> { } density *= DisplayMetrics.DENSITY_DEFAULT_SCALE; + // If bounds have been overridden at this level, restrict config resources to these bounds + // rather than the parent because the overridden bounds can be larger than the parent. + boolean hasOverrideBounds = false; + final Rect resolvedBounds = inOutConfig.windowConfiguration.getBounds(); - if (resolvedBounds == null) { - mTmpFullBounds.setEmpty(); + if (resolvedBounds == null || resolvedBounds.isEmpty()) { + mTmpFullBounds.set(parentConfig.windowConfiguration.getBounds()); } else { mTmpFullBounds.set(resolvedBounds); - } - if (mTmpFullBounds.isEmpty()) { - mTmpFullBounds.set(parentConfig.windowConfiguration.getBounds()); + hasOverrideBounds = true; } Rect outAppBounds = inOutConfig.windowConfiguration.getAppBounds(); @@ -2244,7 +2302,16 @@ class Task extends WindowContainer<WindowContainer> { // the out bounds doesn't need to be restricted by the parent. final boolean insideParentBounds = compatInsets == null; if (insideParentBounds && windowingMode != WINDOWING_MODE_FREEFORM) { - final Rect parentAppBounds = parentConfig.windowConfiguration.getAppBounds(); + Rect parentAppBounds; + if (hasOverrideBounds) { + // Since we overrode the bounds, restrict appBounds to display non-decor rather + // than parent. Otherwise, it won't match the overridden bounds. + final TaskDisplayArea displayArea = getDisplayArea(); + parentAppBounds = displayArea != null + ? displayArea.getConfiguration().windowConfiguration.getAppBounds() : null; + } else { + parentAppBounds = parentConfig.windowConfiguration.getAppBounds(); + } if (parentAppBounds != null && !parentAppBounds.isEmpty()) { outAppBounds.intersect(parentAppBounds); } @@ -2291,13 +2358,13 @@ class Task extends WindowContainer<WindowContainer> { if (inOutConfig.screenWidthDp == Configuration.SCREEN_WIDTH_DP_UNDEFINED) { final int overrideScreenWidthDp = (int) (mTmpStableBounds.width() / density); - inOutConfig.screenWidthDp = insideParentBounds + inOutConfig.screenWidthDp = (insideParentBounds && !hasOverrideBounds) ? Math.min(overrideScreenWidthDp, parentConfig.screenWidthDp) : overrideScreenWidthDp; } if (inOutConfig.screenHeightDp == Configuration.SCREEN_HEIGHT_DP_UNDEFINED) { final int overrideScreenHeightDp = (int) (mTmpStableBounds.height() / density); - inOutConfig.screenHeightDp = insideParentBounds + inOutConfig.screenHeightDp = (insideParentBounds && !hasOverrideBounds) ? Math.min(overrideScreenHeightDp, parentConfig.screenHeightDp) : overrideScreenHeightDp; } @@ -2344,27 +2411,27 @@ class Task extends WindowContainer<WindowContainer> { mTmpBounds.set(getResolvedOverrideConfiguration().windowConfiguration.getBounds()); super.resolveOverrideConfiguration(newParentConfig); - // Resolve override windowing mode to fullscreen for home task (even on freeform - // display), or split-screen-secondary if in split-screen mode. int windowingMode = getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode(); + + // Resolve override windowing mode to fullscreen for home task (even on freeform + // display), or split-screen if in split-screen mode. if (getActivityType() == ACTIVITY_TYPE_HOME && windowingMode == WINDOWING_MODE_UNDEFINED) { final int parentWindowingMode = newParentConfig.windowConfiguration.getWindowingMode(); - windowingMode = parentWindowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY - ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY - : WINDOWING_MODE_FULLSCREEN; + windowingMode = WindowConfiguration.isSplitScreenWindowingMode(parentWindowingMode) + ? parentWindowingMode : WINDOWING_MODE_FULLSCREEN; getResolvedOverrideConfiguration().windowConfiguration.setWindowingMode(windowingMode); } - if (!isLeafTask()) { - // Compute configuration overrides for tasks that created by organizer, so that - // organizer can get the correct configuration from those tasks. - if (mCreatedByOrganizer) { - computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig); - } - return; + if (isLeafTask()) { + resolveLeafOnlyOverrideConfigs(newParentConfig); } + computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig); + } + void resolveLeafOnlyOverrideConfigs(Configuration newParentConfig) { + int windowingMode = + getResolvedOverrideConfiguration().windowConfiguration.getWindowingMode(); if (windowingMode == WINDOWING_MODE_UNDEFINED) { windowingMode = newParentConfig.windowConfiguration.getWindowingMode(); } @@ -2404,7 +2471,6 @@ class Task extends WindowContainer<WindowContainer> { outOverrideBounds.offset(0, offsetTop); } } - computeConfigResourceOverrides(getResolvedOverrideConfiguration(), newParentConfig); } /** @@ -2803,28 +2869,6 @@ class Task extends WindowContainer<WindowContainer> { return boundsChange; } - private void updateSurfaceCrop() { - // Only update the crop if we are drawing shadows on the task. - if (mSurfaceControl == null || !mWmService.mRenderShadowsInCompositor || !isRootTask()) { - return; - } - - if (inSplitScreenWindowingMode()) { - // inherit crop from parent - mTmpRect.setEmpty(); - } else { - getBounds(mTmpRect); - } - - mTmpRect.offsetTo(0, 0); - if (mLastSurfaceCrop.equals(mTmpRect)) { - return; - } - - getPendingTransaction().setWindowCrop(mSurfaceControl, mTmpRect); - mLastSurfaceCrop.set(mTmpRect); - } - @Override public boolean onDescendantOrientationChanged(IBinder freezeDisplayToken, ConfigurationContainer requestingContainer) { @@ -3453,7 +3497,6 @@ class Task extends WindowContainer<WindowContainer> { mTmpDimBoundsRect.offsetTo(0, 0); } - updateSurfaceCrop(); updateShadowsRadius(isFocused(), getPendingTransaction()); if (mDimmer.updateDims(getPendingTransaction(), mTmpDimBoundsRect)) { @@ -4392,19 +4435,20 @@ class Task extends WindowContainer<WindowContainer> { // Let the old organizer know it has lost control. sendTaskVanished(); mTaskOrganizer = organizer; - sendTaskAppeared(); - onTaskOrganizerChanged(); - return true; - } - void taskOrganizerUnregistered() { - mTaskOrganizer = null; - mTaskAppearedSent = false; - mLastTaskOrganizerWindowingMode = -1; - onTaskOrganizerChanged(); - if (mCreatedByOrganizer) { - removeImmediately(); + if (mTaskOrganizer != null) { + sendTaskAppeared(); + } else { + // No longer managed by any organizer. + mTaskAppearedSent = false; + mLastTaskOrganizerWindowingMode = -1; + setForceHidden(FLAG_FORCE_HIDDEN_FOR_TASK_ORG, false /* set */); + if (mCreatedByOrganizer) { + removeImmediately(); + } } + + return true; } /** @@ -4441,14 +4485,6 @@ class Task extends WindowContainer<WindowContainer> { return result; } - private void onTaskOrganizerChanged() { - if (mTaskOrganizer == null) { - // If this task is no longer controlled by a task organizer, then reset the force hidden - // state - setForceHidden(FLAG_FORCE_HIDDEN_FOR_TASK_ORG, false /* set */); - } - } - @Override void setSurfaceControl(SurfaceControl sc) { super.setSurfaceControl(sc); diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index adc50bf70446..306c100e651c 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -218,18 +218,24 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { } void dispose() { - releaseTasks(); + // Move organizer from managing specific windowing modes for (int i = mTaskOrganizersForWindowingMode.size() - 1; i >= 0; --i) { mTaskOrganizersForWindowingMode.valueAt(i).remove(mOrganizer.getBinder()); } - } - private void releaseTasks() { - for (int i = mOrganizedTasks.size() - 1; i >= 0; i--) { - final Task t = mOrganizedTasks.get(i); - removeTask(t); - t.taskOrganizerUnregistered(); + // Update tasks currently managed by this organizer to the next one available if + // possible. + while (!mOrganizedTasks.isEmpty()) { + final Task t = mOrganizedTasks.get(0); + t.updateTaskOrganizerState(true /* forceUpdate */); + if (mOrganizedTasks.contains(t)) { + removeTask(t); + } } + + // Remove organizer state after removing tasks so we get a chance to send + // onTaskVanished. + mTaskOrganizerStates.remove(asBinder()); } void unlinkDeath() { @@ -313,16 +319,11 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { new TaskOrganizerState(organizer, uid)); } - if (orgs.size() == 1) { - // Only in the case where this is the root task organizer for the given - // windowing mode, we add report all existing tasks in that mode to the new - // task organizer. - mService.mRootWindowContainer.forAllTasks((task) -> { - if (task.getWindowingMode() == windowingMode) { - task.updateTaskOrganizerState(true /* forceUpdate */); - } - }); - } + mService.mRootWindowContainer.forAllTasks((task) -> { + if (task.getWindowingMode() == windowingMode) { + task.updateTaskOrganizerState(true /* forceUpdate */); + } + }); } } finally { Binder.restoreCallingIdentity(origId); @@ -335,7 +336,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { - final TaskOrganizerState state = mTaskOrganizerStates.remove(organizer.asBinder()); + final TaskOrganizerState state = mTaskOrganizerStates.get(organizer.asBinder()); if (state == null) { return; } @@ -367,7 +368,9 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { void onTaskVanished(ITaskOrganizer organizer, Task task) { final TaskOrganizerState state = mTaskOrganizerStates.get(organizer.asBinder()); - state.removeTask(task); + if (state != null) { + state.removeTask(task); + } } @Override diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index f55a1b3f6ab3..51095ee85eea 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -219,6 +219,7 @@ import android.view.IOnKeyguardExitResult; import android.view.IPinnedStackListener; import android.view.IRecentsAnimationRunner; import android.view.IRotationWatcher; +import android.view.IScrollCaptureController; import android.view.ISystemGestureExclusionListener; import android.view.IWallpaperVisibilityListener; import android.view.IWindow; @@ -2445,17 +2446,15 @@ public class WindowManagerService extends IWindowManager.Stub if (controls != null) { final int length = Math.min(controls.length, outControls.length); for (int i = 0; i < length; i++) { - final InsetsSourceControl control = controls[i]; - - // Check if we are sending invalid leashes. - final SurfaceControl leash = control != null ? control.getLeash() : null; - if (leash != null && !leash.isValid()) { - Slog.wtf(TAG, leash + " is not valid before sending to " + win, - leash.getReleaseStack()); - } - - outControls[i] = win.isClientLocal() && control != null - ? new InsetsSourceControl(control) : control; + // We will leave the critical section before returning the leash to the client, + // so we need to copy the leash to prevent others release the one that we are + // about to return. + // TODO: We will have an extra copy if the client is not local. + // For now, we rely on GC to release it. + // Maybe we can modify InsetsSourceControl.writeToParcel so it can release + // the extra leash as soon as possible. + outControls[i] = controls[i] != null + ? new InsetsSourceControl(controls[i]) : null; } } } @@ -6837,6 +6836,58 @@ public class WindowManagerService extends IWindowManager.Stub } } + /** + * Forwards a scroll capture request to the appropriate window, if available. + * + * @param displayId the display for the request + * @param behindClient token for a window, used to filter the search to windows behind it + * @param taskId specifies the id of a task the result must belong to or -1 to ignore task ids + * @param controller the controller to receive results; a call to either + * {@link IScrollCaptureController#onClientConnected} or + * {@link IScrollCaptureController#onClientUnavailable}. + */ + public void requestScrollCapture(int displayId, @Nullable IBinder behindClient, int taskId, + IScrollCaptureController controller) { + if (!checkCallingPermission(READ_FRAME_BUFFER, "requestScrollCapture()")) { + throw new SecurityException("Requires READ_FRAME_BUFFER permission"); + } + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + DisplayContent dc = mRoot.getDisplayContent(displayId); + if (dc == null) { + ProtoLog.e(WM_ERROR, + "Invalid displayId for requestScrollCapture: %d", displayId); + controller.onClientUnavailable(); + return; + } + WindowState topWindow = null; + if (behindClient != null) { + topWindow = windowForClientLocked(null, behindClient, /* throwOnError*/ true); + } + WindowState targetWindow = dc.findScrollCaptureTargetWindow(topWindow, taskId); + if (targetWindow == null) { + controller.onClientUnavailable(); + return; + } + // Forward to the window for handling. + try { + targetWindow.mClient.requestScrollCapture(controller); + } catch (RemoteException e) { + ProtoLog.w(WM_ERROR, + "requestScrollCapture: caught exception dispatching to window." + + "token=%s", targetWindow.mClient.asBinder()); + controller.onClientUnavailable(); + } + } + } catch (RemoteException e) { + ProtoLog.w(WM_ERROR, + "requestScrollCapture: caught exception dispatching callback: %s", e); + } finally { + Binder.restoreCallingIdentity(token); + } + } + @Override public void dontOverrideDisplayInfo(int displayId) { final long token = Binder.clearCallingIdentity(); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 83e7ad57e68d..ef690e1db396 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5289,7 +5289,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // to account for it. If we actually have shadows we will // then un-inset ourselves by the surfaceInsets. if (stack != null) { - final int outset = stack.getStackOutset(); + final int outset = stack.getTaskOutset(); outPoint.offset(outset, outset); } diff --git a/services/core/jni/com_android_server_tv_GamepadKeys.h b/services/core/jni/com_android_server_tv_GamepadKeys.h index 11fc9031da3b..127010f907ff 100644 --- a/services/core/jni/com_android_server_tv_GamepadKeys.h +++ b/services/core/jni/com_android_server_tv_GamepadKeys.h @@ -1,77 +1,104 @@ #ifndef ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_ #define ANDROIDTVREMOTE_SERVICE_JNI_GAMEPAD_KEYS_H_ +#include <android/input.h> +#include <android/keycodes.h> #include <linux/input.h> namespace android { -// Follows the W3 spec for gamepad buttons and their corresponding mapping into -// Linux keycodes. Note that gamepads are generally not very well standardized -// and various controllers will result in different buttons. This mapping tries -// to be reasonable. +// The constant array below defines a mapping between "Android" IDs (key code +// within events) and what is being sent through /dev/uinput. // -// W3 Button spec: https://www.w3.org/TR/gamepad/#remapping +// The translation back from uinput key codes into android key codes is done through +// the corresponding key layout files. This file and // -// Standard gamepad keycodes are added plus 2 additional buttons (e.g. Stadia -// has "Assistant" and "Share", PS4 has the touchpad button). +// data/keyboards/Vendor_18d1_Product_0200.kl // -// To generate this list, PS4, XBox, Stadia and Nintendo Switch Pro were tested. -static const int GAMEPAD_KEY_CODES[19] = { - // Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar - BTN_A, // "South", A, GAMEPAD and SOUTH have the same constant - BTN_B, // "East", BTN_B, BTN_EAST have the same constant - BTN_X, // "West", Note that this maps to X and NORTH in constants - BTN_Y, // "North", Note that this maps to Y and WEST in constants +// MUST be kept in sync. +// +// see https://source.android.com/devices/input/key-layout-files for documentation. - BTN_TL, // "Left Bumper" / "L1" - Nintendo sends BTN_WEST instead - BTN_TR, // "Right Bumper" / "R1" - Nintendo sends BTN_Z instead +// Defines axis mapping information between android and +// uinput axis. +struct GamepadKey { + int32_t androidKeyCode; + int linuxUinputKeyCode; +}; + +static const GamepadKey GAMEPAD_KEYS[] = { + // Right-side buttons. A/B/X/Y or circle/triangle/square/X or similar + {AKEYCODE_BUTTON_A, BTN_A}, + {AKEYCODE_BUTTON_B, BTN_B}, + {AKEYCODE_BUTTON_X, BTN_X}, + {AKEYCODE_BUTTON_Y, BTN_Y}, - // For triggers, gamepads vary: - // - Stadia sends analog values over ABS_GAS/ABS_BRAKE and sends - // TriggerHappy3/4 as digital presses - // - PS4 and Xbox send analog values as ABS_Z/ABS_RZ - // - Nintendo Pro sends BTN_TL/BTN_TR (since bumpers behave differently) - // As placeholders we chose the stadia trigger-happy values since TL/TR are - // sent for bumper button presses - BTN_TRIGGER_HAPPY4, // "Left Trigger" / "L2" - BTN_TRIGGER_HAPPY3, // "Right Trigger" / "R2" + // Bumper buttons and digital triggers. Triggers generally have + // both analog versions (GAS and BRAKE output) and digital ones + {AKEYCODE_BUTTON_L1, BTN_TL2}, + {AKEYCODE_BUTTON_L2, BTN_TL}, + {AKEYCODE_BUTTON_R1, BTN_TR2}, + {AKEYCODE_BUTTON_R2, BTN_TR}, - BTN_SELECT, // "Select/Back". Often "options" or similar - BTN_START, // "Start/forward". Often "hamburger" icon + // general actions for controllers + {AKEYCODE_BUTTON_SELECT, BTN_SELECT}, // Options or "..." + {AKEYCODE_BUTTON_START, BTN_START}, // Menu/Hamburger menu + {AKEYCODE_BUTTON_MODE, BTN_MODE}, // "main" button - BTN_THUMBL, // "Left Joystick Pressed" - BTN_THUMBR, // "Right Joystick Pressed" + // Pressing on the joyticks themselves + {AKEYCODE_BUTTON_THUMBL, BTN_THUMBL}, + {AKEYCODE_BUTTON_THUMBR, BTN_THUMBR}, - // For DPads, gamepads generally only send axis changes - // on ABS_HAT0X and ABS_HAT0Y. - KEY_UP, // "Digital Pad up" - KEY_DOWN, // "Digital Pad down" - KEY_LEFT, // "Digital Pad left" - KEY_RIGHT, // "Digital Pad right" + // DPAD digital keys. HAT axis events are generally also sent. + {AKEYCODE_DPAD_UP, KEY_UP}, + {AKEYCODE_DPAD_DOWN, KEY_DOWN}, + {AKEYCODE_DPAD_LEFT, KEY_LEFT}, + {AKEYCODE_DPAD_RIGHT, KEY_RIGHT}, - BTN_MODE, // "Main button" (Stadia/PS/XBOX/Home) + // "Extra" controller buttons: some devices have "share" and "assistant" + {AKEYCODE_BUTTON_1, BTN_TRIGGER_HAPPY1}, + {AKEYCODE_BUTTON_2, BTN_TRIGGER_HAPPY2}, + {AKEYCODE_BUTTON_3, BTN_TRIGGER_HAPPY3}, + {AKEYCODE_BUTTON_4, BTN_TRIGGER_HAPPY4}, + {AKEYCODE_BUTTON_5, BTN_TRIGGER_HAPPY5}, + {AKEYCODE_BUTTON_6, BTN_TRIGGER_HAPPY6}, + {AKEYCODE_BUTTON_7, BTN_TRIGGER_HAPPY7}, + {AKEYCODE_BUTTON_8, BTN_TRIGGER_HAPPY8}, + {AKEYCODE_BUTTON_9, BTN_TRIGGER_HAPPY9}, + {AKEYCODE_BUTTON_10, BTN_TRIGGER_HAPPY10}, + {AKEYCODE_BUTTON_11, BTN_TRIGGER_HAPPY11}, + {AKEYCODE_BUTTON_12, BTN_TRIGGER_HAPPY12}, + {AKEYCODE_BUTTON_13, BTN_TRIGGER_HAPPY13}, + {AKEYCODE_BUTTON_14, BTN_TRIGGER_HAPPY14}, + {AKEYCODE_BUTTON_15, BTN_TRIGGER_HAPPY15}, + {AKEYCODE_BUTTON_16, BTN_TRIGGER_HAPPY16}, - BTN_TRIGGER_HAPPY1, // Extra button: "Assistant" for Stadia - BTN_TRIGGER_HAPPY2, // Extra button: "Share" for Stadia + // Assignment to support global assistant for devices that support it. + {AKEYCODE_ASSIST, KEY_ASSISTANT}, + {AKEYCODE_VOICE_ASSIST, KEY_VOICECOMMAND}, }; -// Defines information for an axis. -struct Axis { - int number; - int rangeMin; - int rangeMax; +// Defines axis mapping information between android and +// uinput axis. +struct GamepadAxis { + int32_t androidAxis; + float androidRangeMin; + float androidRangeMax; + int linuxUinputAxis; + int linuxUinputRangeMin; + int linuxUinputRangeMax; }; // List of all axes supported by a gamepad -static const Axis GAMEPAD_AXES[] = { - {ABS_X, 0, 254}, // Left joystick X - {ABS_Y, 0, 254}, // Left joystick Y - {ABS_RX, 0, 254}, // Right joystick X - {ABS_RY, 0, 254}, // Right joystick Y - {ABS_Z, 0, 254}, // Left trigger - {ABS_RZ, 0, 254}, // Right trigger - {ABS_HAT0X, -1, 1}, // DPad X - {ABS_HAT0Y, -1, 1}, // DPad Y +static const GamepadAxis GAMEPAD_AXES[] = { + {AMOTION_EVENT_AXIS_X, -1, 1, ABS_X, 0, 254}, // Left joystick X + {AMOTION_EVENT_AXIS_Y, -1, 1, ABS_Y, 0, 254}, // Left joystick Y + {AMOTION_EVENT_AXIS_Z, -1, 1, ABS_Z, 0, 254}, // Right joystick X + {AMOTION_EVENT_AXIS_RZ, -1, 1, ABS_RZ, 0, 254}, // Right joystick Y + {AMOTION_EVENT_AXIS_LTRIGGER, 0, 1, ABS_GAS, 0, 254}, // Left trigger + {AMOTION_EVENT_AXIS_RTRIGGER, 0, 1, ABS_BRAKE, 0, 254}, // Right trigger + {AMOTION_EVENT_AXIS_HAT_X, -1, 1, ABS_HAT0X, -1, 1}, // DPad X + {AMOTION_EVENT_AXIS_HAT_Y, -1, 1, ABS_HAT0Y, -1, 1}, // DPad Y }; } // namespace android diff --git a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp index 0e96bd7ae47e..6e2e2c54518b 100644 --- a/services/core/jni/com_android_server_tv_TvUinputBridge.cpp +++ b/services/core/jni/com_android_server_tv_TvUinputBridge.cpp @@ -31,27 +31,38 @@ #include <utils/String8.h> #include <ctype.h> -#include <linux/input.h> -#include <unistd.h> -#include <sys/time.h> -#include <time.h> -#include <stdint.h> -#include <map> #include <fcntl.h> +#include <linux/input.h> #include <linux/uinput.h> #include <signal.h> +#include <stdint.h> #include <sys/inotify.h> #include <sys/stat.h> +#include <sys/time.h> #include <sys/types.h> +#include <time.h> +#include <unistd.h> +#include <unordered_map> #define SLOT_UNKNOWN -1 namespace android { -static std::map<int32_t,int> keysMap; -static std::map<int32_t,int32_t> slotsMap; +#define GOOGLE_VENDOR_ID 0x18d1 + +#define GOOGLE_VIRTUAL_REMOTE_PRODUCT_ID 0x0100 +#define GOOGLE_VIRTUAL_GAMEPAD_PROUCT_ID 0x0200 + +static std::unordered_map<int32_t, int> keysMap; +static std::unordered_map<int32_t, int32_t> slotsMap; static BitSet32 mtSlots; +// Maps android key code to linux key code. +static std::unordered_map<int32_t, int> gamepadAndroidToLinuxKeyMap; + +// Maps an android gamepad axis to the index within the GAMEPAD_AXES array. +static std::unordered_map<int32_t, int> gamepadAndroidAxisToIndexMap; + static void initKeysMap() { if (keysMap.empty()) { for (size_t i = 0; i < NELEM(KEYS); i++) { @@ -60,16 +71,49 @@ static void initKeysMap() { } } +static void initGamepadKeyMap() { + if (gamepadAndroidToLinuxKeyMap.empty()) { + for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) { + gamepadAndroidToLinuxKeyMap[GAMEPAD_KEYS[i].androidKeyCode] = + GAMEPAD_KEYS[i].linuxUinputKeyCode; + } + } + + if (gamepadAndroidAxisToIndexMap.empty()) { + for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { + gamepadAndroidAxisToIndexMap[GAMEPAD_AXES[i].androidAxis] = i; + } + } +} + static int32_t getLinuxKeyCode(int32_t androidKeyCode) { - std::map<int,int>::iterator it = keysMap.find(androidKeyCode); + std::unordered_map<int, int>::iterator it = keysMap.find(androidKeyCode); if (it != keysMap.end()) { return it->second; } return KEY_UNKNOWN; } +static int getGamepadkeyCode(int32_t androidKeyCode) { + std::unordered_map<int32_t, int>::iterator it = + gamepadAndroidToLinuxKeyMap.find(androidKeyCode); + if (it != gamepadAndroidToLinuxKeyMap.end()) { + return it->second; + } + return KEY_UNKNOWN; +} + +static const GamepadAxis* getGamepadAxis(int32_t androidAxisCode) { + std::unordered_map<int32_t, int>::iterator it = + gamepadAndroidAxisToIndexMap.find(androidAxisCode); + if (it == gamepadAndroidToLinuxKeyMap.end()) { + return nullptr; + } + return &GAMEPAD_AXES[it->second]; +} + static int findSlot(int32_t pointerId) { - std::map<int,int>::iterator it = slotsMap.find(pointerId); + std::unordered_map<int, int>::iterator it = slotsMap.find(pointerId); if (it != slotsMap.end()) { return it->second; } @@ -107,7 +151,7 @@ public: // Open /dev/uinput and prepare to register // the device with the given name and unique Id - bool Open(const char* name, const char* uniqueId); + bool Open(const char* name, const char* uniqueId, uint16_t product); // Checks if the current file descriptor is valid bool IsValid() const { return mFd != kInvalidFileDescriptor; } @@ -141,7 +185,7 @@ int UInputDescriptor::Detach() { return fd; } -bool UInputDescriptor::Open(const char* name, const char* uniqueId) { +bool UInputDescriptor::Open(const char* name, const char* uniqueId, uint16_t product) { if (IsValid()) { ALOGE("UInput device already open"); return false; @@ -161,6 +205,8 @@ bool UInputDescriptor::Open(const char* name, const char* uniqueId) { strlcpy(mUinputDescriptor.name, name, UINPUT_MAX_NAME_SIZE); mUinputDescriptor.id.version = 1; mUinputDescriptor.id.bustype = BUS_VIRTUAL; + mUinputDescriptor.id.vendor = GOOGLE_VENDOR_ID; + mUinputDescriptor.id.product = product; // All UInput devices we use process keys ioctl(mFd, UI_SET_EVBIT, EV_KEY); @@ -258,7 +304,7 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId, initKeysMap(); UInputDescriptor descriptor; - if (!descriptor.Open(name, uniqueId)) { + if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_REMOTE_PRODUCT_ID)) { return nullptr; } @@ -277,21 +323,24 @@ NativeConnection* NativeConnection::open(const char* name, const char* uniqueId, NativeConnection* NativeConnection::openGamepad(const char* name, const char* uniqueId) { ALOGI("Registering uinput device %s: gamepad", name); + initGamepadKeyMap(); + UInputDescriptor descriptor; - if (!descriptor.Open(name, uniqueId)) { + if (!descriptor.Open(name, uniqueId, GOOGLE_VIRTUAL_GAMEPAD_PROUCT_ID)) { return nullptr; } // set the keys mapped for gamepads - for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) { - descriptor.EnableKey(GAMEPAD_KEY_CODES[i]); + for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) { + descriptor.EnableKey(GAMEPAD_KEYS[i].linuxUinputKeyCode); } // define the axes that are required descriptor.EnableAxesEvents(); for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { - const Axis& axis = GAMEPAD_AXES[i]; - descriptor.EnableAxis(axis.number, axis.rangeMin, axis.rangeMax); + const GamepadAxis& axis = GAMEPAD_AXES[i]; + descriptor.EnableAxis(axis.linuxUinputAxis, axis.linuxUinputRangeMin, + axis.linuxUinputRangeMax); } if (!descriptor.Create()) { @@ -350,7 +399,7 @@ static void nativeSendKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jb } } -static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyIndex, +static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyCode, jboolean down) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); @@ -359,16 +408,16 @@ static void nativeSendGamepadKey(JNIEnv* env, jclass clazz, jlong ptr, jint keyI return; } - if ((keyIndex < 0) || (keyIndex >= NELEM(GAMEPAD_KEY_CODES))) { - ALOGE("Invalid gamepad key index: %d", keyIndex); + int linuxKeyCode = getGamepadkeyCode(keyCode); + if (linuxKeyCode == KEY_UNKNOWN) { + ALOGE("Gamepad: received an unknown keycode of %d.", keyCode); return; } - - connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[keyIndex], down ? 1 : 0); + connection->sendEvent(EV_KEY, linuxKeyCode, down ? 1 : 0); } static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jint axis, - jint value) { + jfloat value) { NativeConnection* connection = reinterpret_cast<NativeConnection*>(ptr); if (!connection->IsGamepad()) { @@ -376,7 +425,25 @@ static void nativeSendGamepadAxisValue(JNIEnv* env, jclass clazz, jlong ptr, jin return; } - connection->sendEvent(EV_ABS, axis, value); + const GamepadAxis* axisInfo = getGamepadAxis(axis); + if (axisInfo == nullptr) { + ALOGE("Invalid axis: %d", axis); + return; + } + + if (value > axisInfo->androidRangeMax) { + value = axisInfo->androidRangeMax; + } else if (value < axisInfo->androidRangeMin) { + value = axisInfo->androidRangeMin; + } + + // Converts the android range into the device range + float movementPercent = (value - axisInfo->androidRangeMin) / + (axisInfo->androidRangeMax - axisInfo->androidRangeMin); + int axisRawValue = axisInfo->linuxUinputRangeMin + + movementPercent * (axisInfo->linuxUinputRangeMax - axisInfo->linuxUinputRangeMin); + + connection->sendEvent(EV_ABS, axisInfo->linuxUinputAxis, axisRawValue); } static void nativeSendPointerDown(JNIEnv* env, jclass clazz, jlong ptr, @@ -441,18 +508,20 @@ static void nativeClear(JNIEnv* env, jclass clazz, jlong ptr) { } } } else { - for (size_t i = 0; i < NELEM(GAMEPAD_KEY_CODES); i++) { - connection->sendEvent(EV_KEY, GAMEPAD_KEY_CODES[i], 0); + for (size_t i = 0; i < NELEM(GAMEPAD_KEYS); i++) { + connection->sendEvent(EV_KEY, GAMEPAD_KEYS[i].linuxUinputKeyCode, 0); } for (size_t i = 0; i < NELEM(GAMEPAD_AXES); i++) { - const Axis& axis = GAMEPAD_AXES[i]; - if ((axis.number == ABS_Z) || (axis.number == ABS_RZ)) { + const GamepadAxis& axis = GAMEPAD_AXES[i]; + + if ((axis.linuxUinputAxis == ABS_Z) || (axis.linuxUinputAxis == ABS_RZ)) { // Mark triggers unpressed - connection->sendEvent(EV_ABS, axis.number, 0); + connection->sendEvent(EV_ABS, axis.linuxUinputAxis, axis.linuxUinputRangeMin); } else { // Joysticks and dpad rests on center - connection->sendEvent(EV_ABS, axis.number, (axis.rangeMin + axis.rangeMax) / 2); + connection->sendEvent(EV_ABS, axis.linuxUinputAxis, + (axis.linuxUinputRangeMin + axis.linuxUinputRangeMax) / 2); } } } @@ -475,7 +544,7 @@ static JNINativeMethod gUinputBridgeMethods[] = { {"nativeClear", "(J)V", (void*)nativeClear}, {"nativeSendPointerSync", "(J)V", (void*)nativeSendPointerSync}, {"nativeSendGamepadKey", "(JIZ)V", (void*)nativeSendGamepadKey}, - {"nativeSendGamepadAxisValue", "(JII)V", (void*)nativeSendGamepadAxisValue}, + {"nativeSendGamepadAxisValue", "(JIF)V", (void*)nativeSendGamepadAxisValue}, }; int register_android_server_tv_TvUinputBridge(JNIEnv* env) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 2c0d4c0c9208..7b845557cf73 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -161,8 +161,11 @@ import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; +import android.content.IIntentReceiver; +import android.content.IIntentSender; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; import android.content.PermissionChecker; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -171,6 +174,7 @@ import android.content.pm.CrossProfileAppsInternal; import android.content.pm.IPackageDataObserver; import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManagerInternal; @@ -2703,8 +2707,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { final ComponentName doAdminReceiver = doAdmin.info.getComponent(); clearDeviceOwnerLocked(doAdmin, doUserId); Slog.i(LOG_TAG, "Removing admin artifacts..."); - // TODO(b/149075700): Clean up application restrictions in UserManager. removeAdminArtifacts(doAdminReceiver, doUserId); + Slog.i(LOG_TAG, "Uninstalling the DO..."); + uninstallOrDisablePackage(doAdminComponent.getPackageName(), doUserId); Slog.i(LOG_TAG, "Migration complete."); // Note: KeyChain keys are not removed and will remain accessible for the apps that have @@ -2716,6 +2721,47 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { .write(); } + private void uninstallOrDisablePackage(String packageName, int userHandle) { + final ApplicationInfo appInfo; + try { + appInfo = mIPackageManager.getApplicationInfo( + packageName, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE, userHandle); + } catch (RemoteException e) { + // Shouldn't happen. + return; + } + if (appInfo == null) { + Slog.wtf(LOG_TAG, "Failed to get package info for " + packageName); + return; + } + if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + Slog.i(LOG_TAG, String.format( + "Package %s is pre-installed, marking disabled until used", packageName)); + mContext.getPackageManager().setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, 0 /* flags */); + return; + } + + final IIntentSender.Stub mLocalSender = new IIntentSender.Stub() { + @Override + public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, + IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { + final int status = intent.getIntExtra( + PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); + if (status == PackageInstaller.STATUS_SUCCESS) { + Slog.i(LOG_TAG, String.format( + "Package %s uninstalled for user %d", packageName, userHandle)); + } else { + Slog.e(LOG_TAG, String.format( + "Failed to uninstall %s; status: %d", packageName, status)); + } + } + }; + + final PackageInstaller pi = mInjector.getPackageManager(userHandle).getPackageInstaller(); + pi.uninstall(packageName, 0 /* flags */, new IntentSender((IIntentSender) mLocalSender)); + } + private void moveDoPoliciesToProfileParentAdmin(ActiveAdmin doAdmin, ActiveAdmin parentAdmin) { // The following policies can be already controlled via parent instance, skip if so. if (parentAdmin.mPasswordPolicy.quality == PASSWORD_QUALITY_UNSPECIFIED) { @@ -8766,6 +8812,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { saveSettingsLocked(UserHandle.USER_SYSTEM); clearUserPoliciesLocked(userId); clearOverrideApnUnchecked(); + clearApplicationRestrictions(userId); + mInjector.getPackageManagerInternal().clearBlockUninstallForUser(userId); mOwners.clearDeviceOwner(); mOwners.writeDeviceOwner(); @@ -8779,6 +8827,19 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { toggleBackupServiceActive(UserHandle.USER_SYSTEM, true); } + private void clearApplicationRestrictions(int userId) { + // Changing app restrictions involves disk IO, offload it to the background thread. + mBackgroundHandler.post(() -> { + final List<PackageInfo> installedPackageInfos = mInjector.getPackageManager(userId) + .getInstalledPackages(MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE); + final UserHandle userHandle = UserHandle.of(userId); + for (final PackageInfo packageInfo : installedPackageInfos) { + mInjector.getUserManager().setApplicationRestrictions( + packageInfo.packageName, null /* restrictions */, userHandle); + } + }); + } + @Override public boolean setProfileOwner(ComponentName who, String ownerName, int userHandle) { if (!mHasFeature) { @@ -8898,6 +8959,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { policyData.mOwnerInstalledCaCerts.clear(); saveSettingsLocked(userId); clearUserPoliciesLocked(userId); + clearApplicationRestrictions(userId); mOwners.removeProfileOwner(userId); mOwners.writeProfileOwner(userId); deleteTransferOwnershipBundleLocked(userId); diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp index 79699bb77a92..f423119d240a 100644 --- a/services/incremental/IncrementalService.cpp +++ b/services/incremental/IncrementalService.cpp @@ -1610,6 +1610,8 @@ binder::Status IncrementalService::DataLoaderStub::onStatusChanged(MountId mount fsmStep(); + mStatusCondition.notify_all(); + return binder::Status::ok(); } diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index c87ece29800c..763e19bd14ab 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -67,6 +67,7 @@ import com.android.internal.os.BackgroundThread; import com.android.internal.telephony.SmsApplication; import com.android.server.LocalServices; import com.android.server.notification.NotificationManagerInternal; +import com.android.server.notification.ShortcutHelper; import java.util.ArrayList; import java.util.Collections; @@ -497,10 +498,6 @@ public class DataManager { EventStore.CATEGORY_SHORTCUT_BASED, shortcutId); } - private boolean isPersonShortcut(@NonNull ShortcutInfo shortcutInfo) { - return shortcutInfo.getPersons() != null && shortcutInfo.getPersons().length != 0; - } - @VisibleForTesting @WorkerThread void addOrUpdateConversationInfo(@NonNull ShortcutInfo shortcutInfo) { @@ -712,7 +709,8 @@ public class DataManager { @NonNull List<ShortcutInfo> shortcuts, @NonNull UserHandle user) { mInjector.getBackgroundExecutor().execute(() -> { for (ShortcutInfo shortcut : shortcuts) { - if (isPersonShortcut(shortcut)) { + if (ShortcutHelper.isConversationShortcut( + shortcut, mShortcutServiceInternal, user.getIdentifier())) { addOrUpdateConversationInfo(shortcut); } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java index 335217719cc9..064e3486823a 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java @@ -16,6 +16,8 @@ package com.android.server.accessibility; +import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS; + import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -308,7 +310,7 @@ public class SystemActionPerformerTest { AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT); verify(mMockScreenshotHelper).takeScreenshot( eq(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN), anyBoolean(), - anyBoolean(), any(Handler.class), any()); + anyBoolean(), eq(SCREENSHOT_ACCESSIBILITY_ACTIONS), any(Handler.class), any()); } // PendingIntent is a final class and cannot be mocked. So we are using this diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java index f8bcff55ba41..6fe259e7fc85 100644 --- a/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -45,6 +45,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @@ -54,9 +56,11 @@ import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.IApplicationThread; import android.app.IUidObserver; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -70,6 +74,7 @@ import androidx.test.filters.FlakyTest; import androidx.test.filters.MediumTest; import androidx.test.filters.SmallTest; +import com.android.server.LocalServices; import com.android.server.am.ProcessList.IsolatedUidRange; import com.android.server.am.ProcessList.IsolatedUidRangeAllocator; import com.android.server.appop.AppOpsService; @@ -77,6 +82,7 @@ import com.android.server.wm.ActivityTaskManagerService; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; @@ -115,6 +121,18 @@ public class ActivityManagerServiceTest { UidRecord.CHANGE_ACTIVE }; + private static PackageManagerInternal sPackageManagerInternal; + + @BeforeClass + public static void setUpOnce() { + sPackageManagerInternal = mock(PackageManagerInternal.class); + doReturn(new ComponentName("", "")).when(sPackageManagerInternal) + .getSystemUiServiceComponent(); + // Remove stale instance of PackageManagerInternal if there is any + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, sPackageManagerInternal); + } + @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule(); private Context mContext = getInstrumentation().getTargetContext(); @@ -258,8 +276,11 @@ public class ActivityManagerServiceTest { uidRec.hasInternetPermission = true; mAms.mProcessList.mActiveUids.put(uid, uidRec); - final ProcessRecord appRec = new ProcessRecord(mAms, new ApplicationInfo(), TAG, uid); - appRec.thread = Mockito.mock(IApplicationThread.class); + ApplicationInfo info = new ApplicationInfo(); + info.packageName = ""; + + final ProcessRecord appRec = new ProcessRecord(mAms, info, TAG, uid); + appRec.thread = mock(IApplicationThread.class); mAms.mProcessList.mLruProcesses.add(appRec); return uidRec; @@ -497,7 +518,7 @@ public class ActivityManagerServiceTest { }; final IUidObserver[] observers = new IUidObserver.Stub[changesToObserve.length]; for (int i = 0; i < observers.length; ++i) { - observers[i] = Mockito.mock(IUidObserver.Stub.class); + observers[i] = mock(IUidObserver.Stub.class); when(observers[i].asBinder()).thenReturn((IBinder) observers[i]); mAms.registerUidObserver(observers[i], changesToObserve[i] /* which */, ActivityManager.PROCESS_STATE_UNKNOWN /* cutpoint */, null /* caller */); @@ -610,7 +631,7 @@ public class ActivityManagerServiceTest { */ @Test public void testDispatchUidChanges_procStateCutpoint() throws RemoteException { - final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class); + final IUidObserver observer = mock(IUidObserver.Stub.class); when(observer.asBinder()).thenReturn((IBinder) observer); mAms.registerUidObserver(observer, ActivityManager.UID_OBSERVER_PROCSTATE /* which */, @@ -704,7 +725,7 @@ public class ActivityManagerServiceTest { assertEquals("No observers registered, so validateUids should be empty", 0, mAms.mValidateUids.size()); - final IUidObserver observer = Mockito.mock(IUidObserver.Stub.class); + final IUidObserver observer = mock(IUidObserver.Stub.class); when(observer.asBinder()).thenReturn((IBinder) observer); mAms.registerUidObserver(observer, 0, 0, null); // Verify that when observers are registered, then validateUids is correctly updated. diff --git a/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java b/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java index d12d8040183a..b2d7177e04eb 100644 --- a/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java +++ b/services/tests/servicestests/src/com/android/server/am/OomAdjusterTests.java @@ -22,12 +22,15 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import android.app.ActivityManager; import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManagerInternal; import com.android.server.LocalServices; import com.android.server.wm.ActivityTaskManagerService; @@ -45,6 +48,7 @@ import org.junit.Test; public class OomAdjusterTests { private static Context sContext; private static ActivityManagerService sService; + private static PackageManagerInternal sPackageManagerInternal; private ProcessRecord mProcessRecord; @@ -56,6 +60,13 @@ public class OomAdjusterTests { public static void setUpOnce() { sContext = getInstrumentation().getTargetContext(); + sPackageManagerInternal = mock(PackageManagerInternal.class); + doReturn(new ComponentName("", "")).when(sPackageManagerInternal) + .getSystemUiServiceComponent(); + // Remove stale instance of PackageManagerInternal if there is any + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, sPackageManagerInternal); + // We need to run with dexmaker share class loader to make use of // ActivityTaskManagerService from wm package. runWithDexmakerShareClassLoader(() -> { diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java index 74e7f8c44d1a..a0b9d9d2a875 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceMigrationTest.java @@ -62,6 +62,10 @@ public class DevicePolicyManagerServiceMigrationTest extends DpmTestBase { mContext = getContext(); + // Make createContextAsUser to work. + mContext.packageName = "com.android.frameworks.servicestests"; + getServices().addPackageContext(UserHandle.of(0), mContext); + when(getServices().packageManager.hasSystemFeature(eq(PackageManager.FEATURE_DEVICE_ADMIN))) .thenReturn(true); } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 09d1d3a270ba..57039e53429e 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -196,6 +196,11 @@ public class DevicePolicyManagerTest extends DpmTestBase { anyInt(), any(UserHandle.class)); + // Make createContextAsUser to work. + mContext.packageName = "com.android.frameworks.servicestests"; + getServices().addPackageContext(UserHandle.of(0), mContext); + getServices().addPackageContext(UserHandle.of(DpmMockContext.CALLER_USER_HANDLE), mContext); + // By default, pretend all users are running and unlocked. when(getServices().userManager.isUserUnlocked(anyInt())).thenReturn(true); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java index 8625a1ed9fda..20716ab501df 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DpmMockContext.java @@ -460,6 +460,15 @@ public class DpmMockContext extends MockContext { } @Override + public Context createContextAsUser(UserHandle user, int flags) { + try { + return mMockSystemServices.createPackageContextAsUser(packageName, flags, user); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException(e); + } + } + + @Override public ContentResolver getContentResolver() { return mMockSystemServices.contentResolver; } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java new file mode 100644 index 000000000000..c9dbdd2364cc --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStrongAuthTest.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings; + +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT; +import static com.android.server.locksettings.LockSettingsStrongAuth.DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS; +import static com.android.server.locksettings.LockSettingsStrongAuth.DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS; +import static com.android.server.locksettings.LockSettingsStrongAuth.NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG; +import static com.android.server.locksettings.LockSettingsStrongAuth.NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG; +import static com.android.server.locksettings.LockSettingsStrongAuth.STRONG_AUTH_TIMEOUT_ALARM_TAG; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AlarmManager; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricIdleTimeoutAlarmListener; +import com.android.server.locksettings.LockSettingsStrongAuth.NonStrongBiometricTimeoutAlarmListener; +import com.android.server.locksettings.LockSettingsStrongAuth.StrongAuthTimeoutAlarmListener; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +public class LockSettingsStrongAuthTest { + + private static final String TAG = LockSettingsStrongAuthTest.class.getSimpleName(); + + private static final int PRIMARY_USER_ID = 0; + + private LockSettingsStrongAuth mStrongAuth; + private final int mDefaultStrongAuthFlags = STRONG_AUTH_NOT_REQUIRED; + private final boolean mDefaultIsNonStrongBiometricAllowed = true; + + @Mock + private Context mContext; + @Mock + private LockSettingsStrongAuth.Injector mInjector; + @Mock + private AlarmManager mAlarmManager; + @Mock + private DevicePolicyManager mDPM; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mInjector.getAlarmManager(mContext)).thenReturn(mAlarmManager); + when(mInjector.getDefaultStrongAuthFlags(mContext)).thenReturn(mDefaultStrongAuthFlags); + when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn(mDPM); + + mStrongAuth = new LockSettingsStrongAuth(mContext, mInjector); + } + + @Test + public void testScheduleNonStrongBiometricIdleTimeout() { + final long nextAlarmTime = 1000; + when(mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_MS)) + .thenReturn(nextAlarmTime); + mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID); + + waitForIdle(); + NonStrongBiometricIdleTimeoutAlarmListener alarm = mStrongAuth + .mNonStrongBiometricIdleTimeoutAlarmListener.get(PRIMARY_USER_ID); + // verify that a new alarm for idle timeout is added for the user + assertNotNull(alarm); + // verify that the alarm is scheduled + verifyAlarm(nextAlarmTime, NON_STRONG_BIOMETRIC_IDLE_TIMEOUT_ALARM_TAG, alarm); + } + + @Test + public void testSetIsNonStrongBiometricAllowed_disallowed() { + mStrongAuth.setIsNonStrongBiometricAllowed(false /* allowed */, PRIMARY_USER_ID); + + waitForIdle(); + // verify that unlocking with non-strong biometrics is not allowed + assertFalse(mStrongAuth.mIsNonStrongBiometricAllowedForUser + .get(PRIMARY_USER_ID, mDefaultIsNonStrongBiometricAllowed)); + } + + @Test + public void testReportSuccessfulBiometricUnlock_nonStrongBiometric_fallbackTimeout() { + final long nextAlarmTime = 1000; + when(mInjector.getNextAlarmTimeMs(DEFAULT_NON_STRONG_BIOMETRIC_TIMEOUT_MS)) + .thenReturn(nextAlarmTime); + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + NonStrongBiometricTimeoutAlarmListener alarm = + mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(PRIMARY_USER_ID); + // verify that a new alarm for fallback timeout is added for the user + assertNotNull(alarm); + // verify that the alarm is scheduled + verifyAlarm(nextAlarmTime, NON_STRONG_BIOMETRIC_TIMEOUT_ALARM_TAG, alarm); + } + + @Test + public void testRequireStrongAuth_nonStrongBiometric_fallbackTimeout() { + mStrongAuth.requireStrongAuth( + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT /* strongAuthReason */, + PRIMARY_USER_ID); + + waitForIdle(); + // verify that the StrongAuthFlags for the user contains the expected flag + final int expectedFlag = STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT; + verifyStrongAuthFlags(expectedFlag, PRIMARY_USER_ID); + } + + @Test + public void testReportSuccessfulBiometricUnlock_nonStrongBiometric_cancelIdleTimeout() { + // lock device and schedule an alarm for non-strong biometric idle timeout + mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID); + // unlock with non-strong biometric + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + + // verify that the current alarm for idle timeout is cancelled after a successful unlock + verify(mAlarmManager).cancel(any(NonStrongBiometricIdleTimeoutAlarmListener.class)); + } + + @Test + public void testReportSuccessfulBiometricUnlock_strongBio_cancelAlarmsAndAllowNonStrongBio() { + setupAlarms(PRIMARY_USER_ID); + mStrongAuth.reportSuccessfulBiometricUnlock(true /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + // verify that unlocking with strong biometric cancels alarms for fallback and idle timeout + // and re-allow unlocking with non-strong biometric + verifyAlarmsCancelledAndNonStrongBiometricAllowed(PRIMARY_USER_ID); + } + + @Test + public void testReportSuccessfulStrongAuthUnlock_schedulePrimaryAuthTimeout() { + final long nextAlarmTime = 1000; + when(mInjector.getNextAlarmTimeMs(mDPM.getRequiredStrongAuthTimeout(null, PRIMARY_USER_ID))) + .thenReturn(nextAlarmTime); + mStrongAuth.reportSuccessfulStrongAuthUnlock(PRIMARY_USER_ID); + + waitForIdle(); + StrongAuthTimeoutAlarmListener alarm = + mStrongAuth.mStrongAuthTimeoutAlarmListenerForUser.get(PRIMARY_USER_ID); + // verify that a new alarm for primary auth timeout is added for the user + assertNotNull(alarm); + // verify that the alarm is scheduled + verifyAlarm(nextAlarmTime, STRONG_AUTH_TIMEOUT_ALARM_TAG, alarm); + } + + @Test + public void testReportSuccessfulStrongAuthUnlock_cancelAlarmsAndAllowNonStrongBio() { + setupAlarms(PRIMARY_USER_ID); + mStrongAuth.reportSuccessfulStrongAuthUnlock(PRIMARY_USER_ID); + + waitForIdle(); + // verify that unlocking with primary auth (PIN/pattern/password) cancels alarms + // for fallback and idle timeout and re-allow unlocking with non-strong biometric + verifyAlarmsCancelledAndNonStrongBiometricAllowed(PRIMARY_USER_ID); + } + + @Test + public void testFallbackTimeout_convenienceBiometric_weakBiometric() { + // assume that unlock with convenience biometric + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + // assume that unlock again with weak biometric + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + + waitForIdle(); + // verify that the fallback alarm scheduled when unlocking with convenience biometric is + // not affected when unlocking again with weak biometric + verify(mAlarmManager, never()).cancel(any(NonStrongBiometricTimeoutAlarmListener.class)); + assertNotNull(mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(PRIMARY_USER_ID)); + } + + private void verifyAlarm(long when, String tag, AlarmManager.OnAlarmListener alarm) { + verify(mAlarmManager).set( + eq(AlarmManager.ELAPSED_REALTIME), + eq(when), + eq(tag), + eq(alarm), + eq(mStrongAuth.mHandler)); + } + + private void verifyStrongAuthFlags(int reason, int userId) { + final int flags = mStrongAuth.mStrongAuthForUser.get(userId, mDefaultStrongAuthFlags); + Log.d(TAG, "verifyStrongAuthFlags:" + + " reason=" + Integer.toHexString(reason) + + " userId=" + userId + + " flags=" + Integer.toHexString(flags)); + assertTrue(containsFlag(flags, reason)); + } + + private void setupAlarms(int userId) { + // schedule (a) an alarm for non-strong biometric fallback timeout and (b) an alarm for + // non-strong biometric idle timeout, so later we can verify that unlocking with + // strong biometric or primary auth will cancel those alarms + mStrongAuth.reportSuccessfulBiometricUnlock(false /* isStrongBiometric */, PRIMARY_USER_ID); + mStrongAuth.scheduleNonStrongBiometricIdleTimeout(PRIMARY_USER_ID); + } + + private void verifyAlarmsCancelledAndNonStrongBiometricAllowed(int userId) { + // verify that the current alarm for non-strong biometric fallback timeout is cancelled and + // removed + verify(mAlarmManager).cancel(any(NonStrongBiometricTimeoutAlarmListener.class)); + assertNull(mStrongAuth.mNonStrongBiometricTimeoutAlarmListener.get(userId)); + + // verify that the current alarm for non-strong biometric idle timeout is cancelled + verify(mAlarmManager).cancel(any(NonStrongBiometricIdleTimeoutAlarmListener.class)); + + // verify that unlocking with non-strong biometrics is allowed + assertTrue(mStrongAuth.mIsNonStrongBiometricAllowedForUser + .get(userId, mDefaultIsNonStrongBiometricAllowed)); + } + + private static boolean containsFlag(int haystack, int needle) { + return (haystack & needle) != 0; + } + + private static void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java index 728e1492c0d5..e16f3145de0b 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java @@ -215,6 +215,8 @@ public final class DataManagerTest { mDataManager = new DataManager(mContext, mInjector); mDataManager.initialize(); + when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any())).thenReturn(true); verify(mShortcutServiceInternal).addShortcutChangeCallback( mShortcutChangeCallbackCaptor.capture()); mShortcutChangeCallback = mShortcutChangeCallbackCaptor.getValue(); diff --git a/services/tests/servicestests/src/com/android/server/power/AttentionDetectorTest.java b/services/tests/servicestests/src/com/android/server/power/AttentionDetectorTest.java index c4289efe1839..e7e8aca86364 100644 --- a/services/tests/servicestests/src/com/android/server/power/AttentionDetectorTest.java +++ b/services/tests/servicestests/src/com/android/server/power/AttentionDetectorTest.java @@ -158,17 +158,6 @@ public class AttentionDetectorTest extends AndroidTestCase { } @Test - public void testOnUserActivity_disablesSettingIfNotSufficientPermissions() { - when(mPackageManager.checkPermission(any(), any())).thenReturn( - PackageManager.PERMISSION_DENIED); - - registerAttention(); - boolean enabled = Settings.Secure.getIntForUser(getContext().getContentResolver(), - Settings.Secure.ADAPTIVE_SLEEP, 0, UserHandle.USER_CURRENT) == 1; - assertFalse(enabled); - } - - @Test public void testOnUserActivity_doesntCrashIfNoAttentionService() { mAttentionManagerInternal = null; registerAttention(); diff --git a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java index 965304f3c433..21af3563b869 100644 --- a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java @@ -26,6 +26,7 @@ import android.media.tv.ITvInputManager; import android.media.tv.TvInputManager; import android.media.tv.TvInputService; import android.media.tv.tuner.frontend.FrontendSettings; +import android.media.tv.tunerresourcemanager.CasSessionRequest; import android.media.tv.tunerresourcemanager.IResourcesReclaimListener; import android.media.tv.tunerresourcemanager.ResourceClientProfile; import android.media.tv.tunerresourcemanager.TunerDemuxRequest; @@ -34,7 +35,6 @@ import android.media.tv.tunerresourcemanager.TunerFrontendInfo; import android.media.tv.tunerresourcemanager.TunerFrontendRequest; import android.media.tv.tunerresourcemanager.TunerLnbRequest; import android.media.tv.tunerresourcemanager.TunerResourceManager; -import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import androidx.test.InstrumentationRegistry; @@ -236,12 +236,8 @@ public class TunerResourceManagerServiceTest { TunerFrontendRequest request = new TunerFrontendRequest(0 /*clientId*/, FrontendSettings.TYPE_DVBT); int[] frontendHandle = new int[1]; - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isFalse(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isFalse(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0])) .isEqualTo(TunerResourceManager.INVALID_RESOURCE_HANDLE); } @@ -264,12 +260,8 @@ public class TunerResourceManagerServiceTest { TunerFrontendRequest request = new TunerFrontendRequest(clientId[0] /*clientId*/, FrontendSettings.TYPE_DVBT); int[] frontendHandle = new int[1]; - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isFalse(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isFalse(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0])) .isEqualTo(TunerResourceManager.INVALID_RESOURCE_HANDLE); } @@ -296,12 +288,8 @@ public class TunerResourceManagerServiceTest { TunerFrontendRequest request = new TunerFrontendRequest(clientId[0] /*clientId*/, FrontendSettings.TYPE_DVBT); int[] frontendHandle = new int[1]; - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isTrue(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0])) .isEqualTo(0); } @@ -334,23 +322,15 @@ public class TunerResourceManagerServiceTest { int[] frontendHandle = new int[1]; TunerFrontendRequest request = new TunerFrontendRequest(clientId1[0] /*clientId*/, FrontendSettings.TYPE_DVBT); - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isTrue(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0])) .isEqualTo(infos[0].getId()); request = new TunerFrontendRequest(clientId0[0] /*clientId*/, FrontendSettings.TYPE_DVBT); - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isTrue(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0])) .isEqualTo(infos[1].getId()); assertThat(mTunerResourceManagerService.getFrontendResource(infos[1].getId()) @@ -394,32 +374,20 @@ public class TunerResourceManagerServiceTest { TunerFrontendRequest request = new TunerFrontendRequest(clientId0[0] /*clientId*/, FrontendSettings.TYPE_DVBT); int[] frontendHandle = new int[1]; - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isTrue(); request = new TunerFrontendRequest(clientId1[0] /*clientId*/, FrontendSettings.TYPE_DVBT); - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isFalse(); - assertThat(listener.isRelaimed()).isFalse(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isFalse(); + assertThat(listener.isRelaimed()).isFalse(); request = new TunerFrontendRequest(clientId1[0] /*clientId*/, FrontendSettings.TYPE_DVBS); - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isFalse(); - assertThat(listener.isRelaimed()).isFalse(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isFalse(); + assertThat(listener.isRelaimed()).isFalse(); } @Test @@ -456,12 +424,8 @@ public class TunerResourceManagerServiceTest { TunerFrontendRequest request = new TunerFrontendRequest(clientId0[0] /*clientId*/, FrontendSettings.TYPE_DVBT); int[] frontendHandle = new int[1]; - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isTrue(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0])) .isEqualTo(infos[0].getId()); assertThat(mTunerResourceManagerService.getClientProfile(clientId0[0]) @@ -470,12 +434,8 @@ public class TunerResourceManagerServiceTest { request = new TunerFrontendRequest(clientId1[0] /*clientId*/, FrontendSettings.TYPE_DVBS); - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isTrue(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0])) .isEqualTo(infos[1].getId()); assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].getId()) @@ -511,12 +471,8 @@ public class TunerResourceManagerServiceTest { TunerFrontendRequest request = new TunerFrontendRequest(clientId[0] /*clientId*/, FrontendSettings.TYPE_DVBT); int[] frontendHandle = new int[1]; - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isTrue(); int frontendId = mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0]); assertThat(frontendId).isEqualTo(infos[0].getId()); assertThat(mTunerResourceManagerService @@ -534,6 +490,99 @@ public class TunerResourceManagerServiceTest { } @Test + public void requestCasTest_NoCasAvailable_RequestWithHigherPriority() { + // Register clients + ResourceClientProfile[] profiles = new ResourceClientProfile[2]; + profiles[0] = new ResourceClientProfile("0" /*sessionId*/, + TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK); + profiles[1] = new ResourceClientProfile("1" /*sessionId*/, + TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK); + int[] clientPriorities = {100, 500}; + int[] clientId0 = new int[1]; + int[] clientId1 = new int[1]; + TestResourcesReclaimListener listener = new TestResourcesReclaimListener(); + mTunerResourceManagerService.registerClientProfileInternal( + profiles[0], listener, clientId0); + assertThat(clientId0[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID); + mTunerResourceManagerService.getClientProfile(clientId0[0]) + .setPriority(clientPriorities[0]); + mTunerResourceManagerService.registerClientProfileInternal( + profiles[1], new TestResourcesReclaimListener(), clientId1); + assertThat(clientId1[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID); + mTunerResourceManagerService.getClientProfile(clientId1[0]) + .setPriority(clientPriorities[1]); + + // Init cas resources. + mTunerResourceManagerService.updateCasInfoInternal(1 /*casSystemId*/, 2 /*maxSessionNum*/); + + CasSessionRequest request = new CasSessionRequest(clientId0[0], 1 /*casSystemId*/); + int[] casSessionHandle = new int[1]; + // Request for 2 cas sessions. + assertThat(mTunerResourceManagerService + .requestCasSessionInternal(request, casSessionHandle)).isTrue(); + assertThat(mTunerResourceManagerService + .requestCasSessionInternal(request, casSessionHandle)).isTrue(); + assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0])) + .isEqualTo(1); + assertThat(mTunerResourceManagerService.getClientProfile(clientId0[0]) + .getInUseCasSystemId()).isEqualTo(1); + assertThat(mTunerResourceManagerService.getCasResource(1) + .getOwnerClientIds()).isEqualTo(new HashSet<Integer>(Arrays.asList(clientId0[0]))); + assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isTrue(); + + request = new CasSessionRequest(clientId1[0], 1); + assertThat(mTunerResourceManagerService + .requestCasSessionInternal(request, casSessionHandle)).isTrue(); + assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0])) + .isEqualTo(1); + assertThat(mTunerResourceManagerService.getClientProfile(clientId1[0]) + .getInUseCasSystemId()).isEqualTo(1); + assertThat(mTunerResourceManagerService.getClientProfile(clientId0[0]) + .getInUseCasSystemId()).isEqualTo(ClientProfile.INVALID_RESOURCE_ID); + assertThat(mTunerResourceManagerService.getCasResource(1) + .getOwnerClientIds()).isEqualTo(new HashSet<Integer>(Arrays.asList(clientId1[0]))); + assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isFalse(); + assertThat(listener.isRelaimed()).isTrue(); + } + + @Test + public void releaseCasTest() { + // Register clients + ResourceClientProfile[] profiles = new ResourceClientProfile[1]; + profiles[0] = new ResourceClientProfile("0" /*sessionId*/, + TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK); + int[] clientId = new int[1]; + TestResourcesReclaimListener listener = new TestResourcesReclaimListener(); + mTunerResourceManagerService.registerClientProfileInternal(profiles[0], listener, clientId); + assertThat(clientId[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID); + + // Init cas resources. + mTunerResourceManagerService.updateCasInfoInternal(1 /*casSystemId*/, 2 /*maxSessionNum*/); + + CasSessionRequest request = new CasSessionRequest(clientId[0], 1 /*casSystemId*/); + int[] casSessionHandle = new int[1]; + // Request for 1 cas sessions. + assertThat(mTunerResourceManagerService + .requestCasSessionInternal(request, casSessionHandle)).isTrue(); + assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0])) + .isEqualTo(1); + assertThat(mTunerResourceManagerService.getClientProfile(clientId[0]) + .getInUseCasSystemId()).isEqualTo(1); + assertThat(mTunerResourceManagerService.getCasResource(1) + .getOwnerClientIds()).isEqualTo(new HashSet<Integer>(Arrays.asList(clientId[0]))); + assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isFalse(); + + // Release cas + mTunerResourceManagerService.releaseCasSessionInternal(mTunerResourceManagerService + .getCasResource(1), clientId[0]); + assertThat(mTunerResourceManagerService.getClientProfile(clientId[0]) + .getInUseCasSystemId()).isEqualTo(ClientProfile.INVALID_RESOURCE_ID); + assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isFalse(); + assertThat(mTunerResourceManagerService.getCasResource(1) + .getOwnerClientIds()).isEmpty(); + } + + @Test public void requestLnbTest_NoLnbAvailable_RequestWithHigherPriority() { // Register clients ResourceClientProfile[] profiles = new ResourceClientProfile[2]; @@ -562,24 +611,16 @@ public class TunerResourceManagerServiceTest { TunerLnbRequest request = new TunerLnbRequest(clientId0[0]); int[] lnbHandle = new int[1]; - try { - assertThat(mTunerResourceManagerService - .requestLnbInternal(request, lnbHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestLnbInternal(request, lnbHandle)).isTrue(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(lnbHandle[0])) .isEqualTo(lnbIds[0]); assertThat(mTunerResourceManagerService.getClientProfile(clientId0[0]) .getInUseLnbIds()).isEqualTo(new HashSet<Integer>(Arrays.asList(lnbIds[0]))); request = new TunerLnbRequest(clientId1[0]); - try { - assertThat(mTunerResourceManagerService - .requestLnbInternal(request, lnbHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestLnbInternal(request, lnbHandle)).isTrue(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(lnbHandle[0])) .isEqualTo(lnbIds[0]); assertThat(mTunerResourceManagerService.getLnbResource(lnbIds[0]) @@ -608,12 +649,8 @@ public class TunerResourceManagerServiceTest { TunerLnbRequest request = new TunerLnbRequest(clientId[0]); int[] lnbHandle = new int[1]; - try { - assertThat(mTunerResourceManagerService - .requestLnbInternal(request, lnbHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestLnbInternal(request, lnbHandle)).isTrue(); int lnbId = mTunerResourceManagerService.getResourceIdFromHandle(lnbHandle[0]); assertThat(lnbId).isEqualTo(lnbIds[0]); @@ -647,12 +684,8 @@ public class TunerResourceManagerServiceTest { TunerFrontendRequest request = new TunerFrontendRequest(clientId[0] /*clientId*/, FrontendSettings.TYPE_DVBT); int[] frontendHandle = new int[1]; - try { - assertThat(mTunerResourceManagerService - .requestFrontendInternal(request, frontendHandle)).isTrue(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + assertThat(mTunerResourceManagerService + .requestFrontendInternal(request, frontendHandle)).isTrue(); assertThat(mTunerResourceManagerService.getResourceIdFromHandle(frontendHandle[0])) .isEqualTo(infos[0].getId()); assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].getId()) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index ecdd9e548e6a..7a5e2266e62f 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -6074,6 +6074,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Pretend the shortcut exists List<ShortcutInfo> shortcutInfos = new ArrayList<>(); ShortcutInfo info = mock(ShortcutInfo.class); + when(info.getPackage()).thenReturn(PKG); + when(info.getId()).thenReturn("someshortcutId"); + when(info.getUserId()).thenReturn(USER_SYSTEM); when(info.isLongLived()).thenReturn(true); when(info.isEnabled()).thenReturn(true); shortcutInfos.add(info); @@ -6137,6 +6140,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Pretend the shortcut exists List<ShortcutInfo> shortcutInfos = new ArrayList<>(); ShortcutInfo info = mock(ShortcutInfo.class); + when(info.getPackage()).thenReturn(PKG); + when(info.getId()).thenReturn("someshortcutId"); + when(info.getUserId()).thenReturn(USER_SYSTEM); when(info.isLongLived()).thenReturn(true); when(info.isEnabled()).thenReturn(true); shortcutInfos.add(info); @@ -6483,6 +6489,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mPreferencesHelper.getConversations(anyString(), anyInt())).thenReturn(convos); ShortcutInfo si = mock(ShortcutInfo.class); + when(si.getPackage()).thenReturn(PKG_P); + when(si.getId()).thenReturn("convo"); + when(si.getUserId()).thenReturn(USER_SYSTEM); when(si.getShortLabel()).thenReturn("Hello"); when(si.isLongLived()).thenReturn(true); when(si.isEnabled()).thenReturn(true); @@ -6514,6 +6523,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { when(mPreferencesHelper.getConversations(anyString(), anyInt())).thenReturn(convos); ShortcutInfo si = mock(ShortcutInfo.class); + when(si.getPackage()).thenReturn(PKG_P); + when(si.getId()).thenReturn("convo"); + when(si.getUserId()).thenReturn(USER_SYSTEM); when(si.getShortLabel()).thenReturn("Hello"); when(si.isLongLived()).thenReturn(false); when(mLauncherApps.getShortcuts(any(), any())).thenReturn(Arrays.asList(si)); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java index f7304bd0075b..3095c87ba2f6 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ShortcutHelperTest.java @@ -170,6 +170,9 @@ public class ShortcutHelperTest extends UiServiceTestCase { @Test public void testGetValidShortcutInfo_notLongLived() { ShortcutInfo si = mock(ShortcutInfo.class); + when(si.getPackage()).thenReturn(PKG); + when(si.getId()).thenReturn(SHORTCUT_ID); + when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM); when(si.isLongLived()).thenReturn(false); when(si.isEnabled()).thenReturn(true); ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); @@ -184,6 +187,9 @@ public class ShortcutHelperTest extends UiServiceTestCase { @Test public void testGetValidShortcutInfo_notSharingShortcut() { ShortcutInfo si = mock(ShortcutInfo.class); + when(si.getPackage()).thenReturn(PKG); + when(si.getId()).thenReturn(SHORTCUT_ID); + when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM); when(si.isLongLived()).thenReturn(true); when(si.isEnabled()).thenReturn(true); ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); @@ -198,6 +204,9 @@ public class ShortcutHelperTest extends UiServiceTestCase { @Test public void testGetValidShortcutInfo_notEnabled() { ShortcutInfo si = mock(ShortcutInfo.class); + when(si.getPackage()).thenReturn(PKG); + when(si.getId()).thenReturn(SHORTCUT_ID); + when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM); when(si.isLongLived()).thenReturn(true); when(si.isEnabled()).thenReturn(false); ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); @@ -212,6 +221,9 @@ public class ShortcutHelperTest extends UiServiceTestCase { @Test public void testGetValidShortcutInfo_isValid() { ShortcutInfo si = mock(ShortcutInfo.class); + when(si.getPackage()).thenReturn(PKG); + when(si.getId()).thenReturn(SHORTCUT_ID); + when(si.getUserId()).thenReturn(UserHandle.USER_SYSTEM); when(si.isLongLived()).thenReturn(true); when(si.isEnabled()).thenReturn(true); ArrayList<ShortcutInfo> shortcuts = new ArrayList<>(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java index 6ae8313e39dd..0700f9f2b29c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java @@ -329,6 +329,7 @@ class ActivityTestsBase extends SystemServiceTestsBase { private boolean mCreateStack = true; private ActivityStack mStack; + private TaskDisplayArea mTaskDisplayArea; TaskBuilder(ActivityStackSupervisor supervisor) { mSupervisor = supervisor; @@ -378,9 +379,16 @@ class ActivityTestsBase extends SystemServiceTestsBase { return this; } + TaskBuilder setDisplay(DisplayContent display) { + mTaskDisplayArea = display.getDefaultTaskDisplayArea(); + return this; + } + Task build() { if (mStack == null && mCreateStack) { - mStack = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea().createStack( + TaskDisplayArea displayArea = mTaskDisplayArea != null ? mTaskDisplayArea + : mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea(); + mStack = displayArea.createStack( WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */); spyOn(mStack); } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index daff14992e94..80fcf2e121f4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -41,6 +41,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE; +import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR; import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; @@ -74,6 +75,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import android.annotation.SuppressLint; +import android.app.ActivityTaskManager; import android.app.WindowConfiguration; import android.content.res.Configuration; import android.graphics.Rect; @@ -1207,6 +1209,31 @@ public class DisplayContentTests extends WindowTestsBase { assertNull(taskDisplayArea.getOrCreateRootHomeTask()); } + @Test + public void testFindScrollCaptureTargetWindow_behindWindow() { + DisplayContent display = createNewDisplay(); + ActivityStack stack = createTaskStackOnDisplay(display); + Task task = createTaskInStack(stack, 0 /* userId */); + WindowState activityWindow = createAppWindow(task, TYPE_APPLICATION, "App Window"); + WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot"); + + WindowState result = display.findScrollCaptureTargetWindow(behindWindow, + ActivityTaskManager.INVALID_TASK_ID); + assertEquals(activityWindow, result); + } + + @Test + public void testFindScrollCaptureTargetWindow_taskId() { + DisplayContent display = createNewDisplay(); + ActivityStack stack = createTaskStackOnDisplay(display); + Task task = createTaskInStack(stack, 0 /* userId */); + WindowState window = createAppWindow(task, TYPE_APPLICATION, "App Window"); + WindowState behindWindow = createWindow(null, TYPE_SCREENSHOT, display, "Screenshot"); + + WindowState result = display.findScrollCaptureTargetWindow(null, task.mTaskId); + assertEquals(window, result); + } + private boolean isOptionsPanelAtRight(int displayId) { return (mWm.getPreferredOptionsPanelGravity(displayId) & Gravity.RIGHT) == Gravity.RIGHT; } diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java index be2559719438..e887be0c48c2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java @@ -33,6 +33,7 @@ import androidx.test.filters.SmallTest; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; /** * Build/Install/Run: @@ -40,6 +41,7 @@ import org.junit.Test; */ @SmallTest @Presubmit +@RunWith(WindowTestRunner.class) @FlakyTest public class RefreshRatePolicyTest extends WindowTestsBase { diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java index 519ac780bd6b..dcc2ff1311a5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskRecordTests.java @@ -54,10 +54,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.app.TaskInfo; @@ -75,12 +72,10 @@ import android.util.DisplayMetrics; import android.util.Xml; import android.view.DisplayInfo; -import androidx.test.filters.FlakyTest; import androidx.test.filters.MediumTest; import com.android.internal.app.IVoiceInteractor; import com.android.server.wm.Task.TaskFactory; -import com.android.server.wm.utils.WmDisplayCutout; import org.junit.Before; import org.junit.Test; @@ -368,25 +363,38 @@ public class TaskRecordTests extends ActivityTestsBase { @Test public void testComputeConfigResourceOverrides() { - final Task task = new TaskBuilder(mSupervisor).build(); + final Rect fullScreenBounds = new Rect(0, 0, 1080, 1920); + TestDisplayContent display = new TestDisplayContent.Builder( + mService, fullScreenBounds.width(), fullScreenBounds.height()).build(); + final Task task = new TaskBuilder(mSupervisor).setDisplay(display).build(); final Configuration inOutConfig = new Configuration(); final Configuration parentConfig = new Configuration(); final int longSide = 1200; final int shortSide = 600; + final Rect parentBounds = new Rect(0, 0, 250, 500); + parentConfig.windowConfiguration.setBounds(parentBounds); parentConfig.densityDpi = 400; - parentConfig.screenHeightDp = 200; // 200 * 400 / 160 = 500px - parentConfig.screenWidthDp = 100; // 100 * 400 / 160 = 250px + parentConfig.screenHeightDp = (parentBounds.bottom * 160) / parentConfig.densityDpi; // 200 + parentConfig.screenWidthDp = (parentBounds.right * 160) / parentConfig.densityDpi; // 100 parentConfig.windowConfiguration.setRotation(ROTATION_0); - // Portrait bounds. - inOutConfig.windowConfiguration.getBounds().set(0, 0, shortSide, longSide); - // By default, the parent bounds should limit the existing input bounds. + // By default, the input bounds will fill parent. task.computeConfigResourceOverrides(inOutConfig, parentConfig); assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp); assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp); assertEquals(Configuration.ORIENTATION_PORTRAIT, inOutConfig.orientation); + // If bounds are overridden, config properties should be made to match. Surface hierarchy + // will crop for policy. + inOutConfig.setToDefaults(); + inOutConfig.windowConfiguration.getBounds().set(0, 0, shortSide, longSide); + // By default, the parent bounds should limit the existing input bounds. + task.computeConfigResourceOverrides(inOutConfig, parentConfig); + + assertEquals(longSide, inOutConfig.screenHeightDp * parentConfig.densityDpi / 160); + assertEquals(shortSide, inOutConfig.screenWidthDp * parentConfig.densityDpi / 160); + inOutConfig.setToDefaults(); // Landscape bounds. inOutConfig.windowConfiguration.getBounds().set(0, 0, longSide, shortSide); @@ -394,21 +402,17 @@ public class TaskRecordTests extends ActivityTestsBase { // Setup the display with a top stable inset. The later assertion will ensure the inset is // excluded from screenHeightDp. final int statusBarHeight = 100; - final DisplayContent displayContent = task.mDisplayContent; - final DisplayPolicy policy = mock(DisplayPolicy.class); + final DisplayPolicy policy = display.getDisplayPolicy(); doAnswer(invocationOnMock -> { final Rect insets = invocationOnMock.<Rect>getArgument(0); insets.top = statusBarHeight; return null; }).when(policy).convertNonDecorInsetsToStableInsets(any(), eq(ROTATION_0)); - doReturn(policy).when(displayContent).getDisplayPolicy(); - doReturn(mock(WmDisplayCutout.class)).when(displayContent) - .calculateDisplayCutoutForRotation(anyInt()); // Without limiting to be inside the parent bounds, the out screen size should keep relative // to the input bounds. final ActivityRecord.CompatDisplayInsets compatIntsets = - new ActivityRecord.CompatDisplayInsets(displayContent, task); + new ActivityRecord.CompatDisplayInsets(display, task); task.computeConfigResourceOverrides(inOutConfig, parentConfig, compatIntsets); assertEquals((shortSide - statusBarHeight) * DENSITY_DEFAULT / parentConfig.densityDpi, @@ -454,7 +458,6 @@ public class TaskRecordTests extends ActivityTestsBase { parentConfig.screenWidthDp = 100; // 100 * 400 / 160 = 250px parentConfig.windowConfiguration.setRotation(ROTATION_0); - final float density = 2.5f; // densityDpi / DENSITY_DEFAULT_SCALE = 400 / 160.0f final int longSideDp = 480; // longSide / density = 1200 / 400 * 160 final int shortSideDp = 240; // shortSide / density = 600 / 400 * 160 final int screenLayout = parentConfig.screenLayout @@ -463,31 +466,38 @@ public class TaskRecordTests extends ActivityTestsBase { Configuration.reduceScreenLayout(screenLayout, longSideDp, shortSideDp); // Portrait bounds overlapping with navigation bar, without insets. - inOutConfig.windowConfiguration.getBounds().set(0, + final Rect freeformBounds = new Rect(0, displayHeight - 10 - longSide, shortSide, displayHeight - 10); + inOutConfig.windowConfiguration.setBounds(freeformBounds); // Set to freeform mode to verify bug fix. inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); task.computeConfigResourceOverrides(inOutConfig, parentConfig); - assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp); - assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp); + // screenW/H should not be effected by parent since overridden and freeform + assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi, + inOutConfig.screenWidthDp); + assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi, + inOutConfig.screenHeightDp); assertEquals(reducedScreenLayout, inOutConfig.screenLayout); inOutConfig.setToDefaults(); // Landscape bounds overlapping with navigtion bar, without insets. - inOutConfig.windowConfiguration.getBounds().set(0, + freeformBounds.set(0, displayHeight - 10 - shortSide, longSide, displayHeight - 10); + inOutConfig.windowConfiguration.setBounds(freeformBounds); inOutConfig.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); task.computeConfigResourceOverrides(inOutConfig, parentConfig); - assertEquals(parentConfig.screenWidthDp, inOutConfig.screenWidthDp); - assertEquals(parentConfig.screenHeightDp, inOutConfig.screenHeightDp); + assertEquals(freeformBounds.width() * 160 / parentConfig.densityDpi, + inOutConfig.screenWidthDp); + assertEquals(freeformBounds.height() * 160 / parentConfig.densityDpi, + inOutConfig.screenHeightDp); assertEquals(reducedScreenLayout, inOutConfig.screenLayout); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java index 7cb5e84e4e48..f354a04101f5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackTests.java @@ -186,7 +186,7 @@ public class TaskStackTests extends WindowTestsBase { final ActivityStack stack = createTaskStackOnDisplay(mDisplayContent); final int stackOutset = 10; spyOn(stack); - doReturn(stackOutset).when(stack).getStackOutset(); + doReturn(stackOutset).when(stack).getTaskOutset(); doReturn(true).when(stack).inMultiWindowMode(); // Mock the resolved override windowing mode to non-fullscreen diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java index 91c3c2782d94..e39b4bcd2eb0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java @@ -24,6 +24,7 @@ import android.os.RemoteException; import android.util.MergedConfiguration; import android.view.DisplayCutout; import android.view.DragEvent; +import android.view.IScrollCaptureController; import android.view.IWindow; import android.view.InsetsSourceControl; import android.view.InsetsState; @@ -113,6 +114,10 @@ public class TestIWindow extends IWindow.Stub { } @Override + public void requestScrollCapture(IScrollCaptureController controller) throws RemoteException { + } + + @Override public void showInsets(int types, boolean fromIme) throws RemoteException { } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 53cc09bf08e8..7d2e88014f45 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -44,7 +44,6 @@ import static com.android.server.wm.WindowContainer.POSITION_TOP; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -82,7 +81,7 @@ import java.util.List; * Test class for {@link ITaskOrganizer} and {@link android.window.ITaskOrganizerController}. * * Build/Install/Run: - * atest WmTests:TaskOrganizerTests + * atest WmTests:WindowOrganizerTests */ @SmallTest @Presubmit @@ -265,15 +264,22 @@ public class WindowOrganizerTests extends WindowTestsBase { // newly entering the windowing mode. final ITaskOrganizer organizer2 = registerMockOrganizer(WINDOWING_MODE_MULTI_WINDOW); stack2.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - verify(organizer2).onTaskAppeared(any()); + // One each for task and task2 + verify(organizer2, times(2)).onTaskAppeared(any()); + verify(organizer2, times(0)).onTaskVanished(any()); + // One for task + verify(organizer).onTaskVanished(any()); assertTrue(stack2.isOrganized()); // Now we unregister the second one, the first one should automatically be reregistered // so we verify that it's now seeing changes. mWm.mAtmService.mTaskOrganizerController.unregisterTaskOrganizer(organizer2); + verify(organizer, times(3)).onTaskAppeared(any()); + verify(organizer2, times(2)).onTaskVanished(any()); stack3.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); - verify(organizer, times(2)).onTaskAppeared(any()); + verify(organizer, times(4)).onTaskAppeared(any()); + verify(organizer2, times(2)).onTaskVanished(any()); assertTrue(stack3.isOrganized()); } @@ -407,22 +413,20 @@ public class WindowOrganizerTests extends WindowTestsBase { .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); final Task task = stack.getTopMostTask(); WindowContainerTransaction t = new WindowContainerTransaction(); - t.setBounds(task.mRemoteToken.toWindowContainerToken(), new Rect(10, 10, 100, 100)); mWm.mAtmService.mWindowOrganizerController.applyTransaction(t); final int origScreenWDp = task.getConfiguration().screenHeightDp; final int origScreenHDp = task.getConfiguration().screenHeightDp; t = new WindowContainerTransaction(); // verify that setting config overrides on parent restricts children. t.setScreenSizeDp(stack.mRemoteToken - .toWindowContainerToken(), origScreenWDp, origScreenHDp); - t.setBounds(task.mRemoteToken.toWindowContainerToken(), new Rect(10, 10, 150, 200)); + .toWindowContainerToken(), origScreenWDp, origScreenHDp / 2); mWm.mAtmService.mWindowOrganizerController.applyTransaction(t); - assertEquals(origScreenHDp, task.getConfiguration().screenHeightDp); + assertEquals(origScreenHDp / 2, task.getConfiguration().screenHeightDp); t = new WindowContainerTransaction(); t.setScreenSizeDp(stack.mRemoteToken.toWindowContainerToken(), SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); mWm.mAtmService.mWindowOrganizerController.applyTransaction(t); - assertNotEquals(origScreenHDp, task.getConfiguration().screenHeightDp); + assertEquals(origScreenHDp, task.getConfiguration().screenHeightDp); } @Test @@ -905,12 +909,13 @@ public class WindowOrganizerTests extends WindowTestsBase { task.setHasBeenVisible(true); verify(organizer, times(1)).onTaskAppeared(any()); - task.taskOrganizerUnregistered(); + task.setTaskOrganizer(null); + verify(organizer, times(1)).onTaskVanished(any()); task.setTaskOrganizer(organizer); verify(organizer, times(2)).onTaskAppeared(any()); task.removeImmediately(); - verify(organizer).onTaskVanished(any()); + verify(organizer, times(2)).onTaskVanished(any()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index e561c13a4e99..6a64d1c976c4 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -247,7 +247,7 @@ class WindowTestsBase extends SystemServiceTestsBase { WindowState createAppWindow(Task task, int type, String name) { synchronized (mWm.mGlobalLock) { final ActivityRecord activity = - WindowTestUtils.createTestActivityRecord(mDisplayContent); + WindowTestUtils.createTestActivityRecord(task.getDisplayContent()); task.addChild(activity, 0); return createWindow(null, type, activity, name); } diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java index 9dfa3ac7a5d8..fa9909547fc4 100755 --- a/telecomm/java/android/telecom/Connection.java +++ b/telecomm/java/android/telecom/Connection.java @@ -734,6 +734,31 @@ public abstract class Connection extends Conferenceable { "android.telecom.extra.ORIGINAL_CONNECTION_ID"; /** + * Extra key set on a {@link Connection} when it was created via a remote connection service. + * For example, if a connection manager requests a remote connection service to create a call + * using one of the remote connection service's phone account handle, this extra will be set so + * that Telecom knows that the wrapped remote connection originated in a remote connection + * service. We stash this in the extras since connection managers will typically copy the + * extras from a {@link RemoteConnection} to a {@link Connection} (there is ultimately not + * other way to relate a {@link RemoteConnection} to a {@link Connection}. + * @hide + */ + public static final String EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE = + "android.telecom.extra.REMOTE_PHONE_ACCOUNT_HANDLE"; + + /** + * Extra key set from a {@link ConnectionService} when using the remote connection APIs + * (e.g. {@link RemoteConnectionService#createRemoteConnection(PhoneAccountHandle, + * ConnectionRequest, boolean)}) to create a remote connection. Provides the receiving + * {@link ConnectionService} with a means to know the package name of the requesting + * {@link ConnectionService} so that {@link #EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE} can be set for + * better visibility in Telecom of where a connection ultimately originated. + * @hide + */ + public static final String EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME = + "android.telecom.extra.REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME"; + + /** * Boolean connection extra key set on the extras passed to * {@link Connection#sendConnectionEvent} which indicates that audio is present * on the RTT call when the extra value is true. diff --git a/telecomm/java/android/telecom/ConnectionService.java b/telecomm/java/android/telecom/ConnectionService.java index 1b60e4820ad0..a716b37f7efd 100755 --- a/telecomm/java/android/telecom/ConnectionService.java +++ b/telecomm/java/android/telecom/ConnectionService.java @@ -1859,9 +1859,25 @@ public abstract class ConnectionService extends Service { new DisconnectCause(DisconnectCause.ERROR, "IMPL_RETURNED_NULL_CONFERENCE"), request.getAccountHandle()); } - if (conference.getExtras() != null) { - conference.getExtras().putString(Connection.EXTRA_ORIGINAL_CONNECTION_ID, callId); + + Bundle extras = request.getExtras(); + Bundle newExtras = new Bundle(); + newExtras.putString(Connection.EXTRA_ORIGINAL_CONNECTION_ID, callId); + if (extras != null) { + // If the request originated from a remote connection service, we will add some + // tracking information that Telecom can use to keep informed of which package + // made the remote request, and which remote connection service was used. + if (extras.containsKey(Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME)) { + newExtras.putString( + Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME, + extras.getString( + Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME)); + newExtras.putParcelable(Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE, + request.getAccountHandle()); + } } + conference.putExtras(newExtras); + mConferenceById.put(callId, conference); mIdByConference.put(conference, callId); conference.addListener(mConferenceListener); @@ -1936,6 +1952,30 @@ public abstract class ConnectionService extends Service { Log.i(this, "createConnection, implementation returned null connection."); connection = Connection.createFailedConnection( new DisconnectCause(DisconnectCause.ERROR, "IMPL_RETURNED_NULL_CONNECTION")); + } else { + try { + Bundle extras = request.getExtras(); + if (extras != null) { + // If the request originated from a remote connection service, we will add some + // tracking information that Telecom can use to keep informed of which package + // made the remote request, and which remote connection service was used. + if (extras.containsKey( + Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME)) { + Bundle newExtras = new Bundle(); + newExtras.putString( + Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME, + extras.getString( + Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME + )); + newExtras.putParcelable(Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE, + request.getAccountHandle()); + connection.putExtras(newExtras); + } + } + } catch (UnsupportedOperationException ose) { + // Do nothing; if the ConnectionService reported a failure it will be an instance + // of an immutable Connection which we cannot edit, so we're out of luck. + } } boolean isSelfManaged = diff --git a/telecomm/java/android/telecom/Logging/Session.java b/telecomm/java/android/telecom/Logging/Session.java index d82e93fac76d..8d3f4e1df8bc 100644 --- a/telecomm/java/android/telecom/Logging/Session.java +++ b/telecomm/java/android/telecom/Logging/Session.java @@ -427,7 +427,7 @@ public class Session { StringBuilder methodName = new StringBuilder(); methodName.append(getFullMethodPath(false /*truncatePath*/)); if (mOwnerInfo != null && !mOwnerInfo.isEmpty()) { - methodName.append("(InCall package: "); + methodName.append("("); methodName.append(mOwnerInfo); methodName.append(")"); } diff --git a/telecomm/java/android/telecom/RemoteConnectionService.java b/telecomm/java/android/telecom/RemoteConnectionService.java index cad5b707a146..a0833011715d 100644 --- a/telecomm/java/android/telecom/RemoteConnectionService.java +++ b/telecomm/java/android/telecom/RemoteConnectionService.java @@ -258,6 +258,9 @@ final class RemoteConnectionService { // See comments on Connection.EXTRA_ORIGINAL_CONNECTION_ID for more information. Bundle newExtras = new Bundle(); newExtras.putString(Connection.EXTRA_ORIGINAL_CONNECTION_ID, callId); + // Track the fact this request was relayed through the remote connection service. + newExtras.putParcelable(Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE, + parcel.getPhoneAccount()); conference.putExtras(newExtras); conference.registerCallback(new RemoteConference.Callback() { @@ -383,6 +386,11 @@ final class RemoteConnectionService { RemoteConnection remoteConnection = new RemoteConnection(callId, mOutgoingConnectionServiceRpc, connection, callingPackage, callingTargetSdkVersion); + // Track that it is via a remote connection. + Bundle newExtras = new Bundle(); + newExtras.putParcelable(Connection.EXTRA_REMOTE_PHONE_ACCOUNT_HANDLE, + connection.getPhoneAccount()); + remoteConnection.putExtras(newExtras); mConnectionById.put(callId, remoteConnection); remoteConnection.registerCallback(new RemoteConnection.Callback() { @Override @@ -535,10 +543,20 @@ final class RemoteConnectionService { ConnectionRequest request, boolean isIncoming) { final String id = UUID.randomUUID().toString(); + Bundle extras = new Bundle(); + if (request.getExtras() != null) { + extras.putAll(request.getExtras()); + } + // We will set the package name for the originator of the remote request; this lets the + // receiving ConnectionService know that the request originated from a remote connection + // service so that it can provide tracking information for Telecom. + extras.putString(Connection.EXTRA_REMOTE_CONNECTION_ORIGINATING_PACKAGE_NAME, + mOurConnectionServiceImpl.getApplicationContext().getOpPackageName()); + final ConnectionRequest newRequest = new ConnectionRequest.Builder() .setAccountHandle(request.getAccountHandle()) .setAddress(request.getAddress()) - .setExtras(request.getExtras()) + .setExtras(extras) .setVideoState(request.getVideoState()) .setRttPipeFromInCall(request.getRttPipeFromInCall()) .setRttPipeToInCall(request.getRttPipeToInCall()) diff --git a/telephony/java/android/telephony/ims/ImsRcsManager.java b/telephony/java/android/telephony/ims/ImsRcsManager.java index 151fb59b7550..ede67dd9fd61 100644 --- a/telephony/java/android/telephony/ims/ImsRcsManager.java +++ b/telephony/java/android/telephony/ims/ImsRcsManager.java @@ -49,7 +49,7 @@ import java.util.function.Consumer; * * Use {@link ImsManager#getImsRcsManager(int)} to create an instance of this manager. */ -public class ImsRcsManager implements RegistrationManager { +public class ImsRcsManager { private static final String TAG = "ImsRcsManager"; /** @@ -173,11 +173,11 @@ public class ImsRcsManager implements RegistrationManager { /** * @hide */ - @Override + // @Override add back to RegistrationManager interface once public. @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void registerImsRegistrationCallback( @NonNull @CallbackExecutor Executor executor, - @NonNull RegistrationCallback c) + @NonNull RegistrationManager.RegistrationCallback c) throws ImsException { if (c == null) { throw new IllegalArgumentException("Must include a non-null RegistrationCallback."); @@ -204,7 +204,7 @@ public class ImsRcsManager implements RegistrationManager { /** * @hide */ - @Override + // @Override add back to RegistrationManager interface once public. @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void unregisterImsRegistrationCallback( @NonNull RegistrationManager.RegistrationCallback c) { @@ -228,10 +228,10 @@ public class ImsRcsManager implements RegistrationManager { /** * @hide */ - @Override + // @Override add back to RegistrationManager interface once public. @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void getRegistrationState(@NonNull @CallbackExecutor Executor executor, - @NonNull @ImsRegistrationState Consumer<Integer> stateCallback) { + @NonNull @RegistrationManager.ImsRegistrationState Consumer<Integer> stateCallback) { if (stateCallback == null) { throw new IllegalArgumentException("Must include a non-null stateCallback."); } @@ -260,7 +260,6 @@ public class ImsRcsManager implements RegistrationManager { /** * @hide */ - @Override @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void getRegistrationTransportType(@NonNull @CallbackExecutor Executor executor, @NonNull @AccessNetworkConstants.TransportType @@ -347,8 +346,7 @@ public class ImsRcsManager implements RegistrationManager { * inactive subscription, it will result in a no-op. * @param c The RCS {@link AvailabilityCallback} to be removed. * @see #registerRcsAvailabilityCallback(Executor, AvailabilityCallback) - * @throws ImsException if the IMS service is not available when calling this method - * {@link ImsRcsController#unregisterRcsAvailabilityCallback()}. + * @throws ImsException if the IMS service is not available when calling this method. * See {@link ImsException#getCode()} for more information on the error codes. * @hide */ @@ -390,8 +388,7 @@ public class ImsRcsManager implements RegistrationManager { * rather the subscription is capable of this service over IMS. * @see #isAvailable(int) * @see android.telephony.CarrierConfigManager#KEY_USE_RCS_PRESENCE_BOOL - * @throws ImsException if the IMS service is not available when calling this method - * {@link ImsRcsController#isCapable(int, int)}. + * @throws ImsException if the IMS service is not available when calling this method. * See {@link ImsException#getCode()} for more information on the error codes. * @hide */ @@ -424,9 +421,8 @@ public class ImsRcsManager implements RegistrationManager { * @return true if the RCS capability is currently available for the associated subscription, * false otherwise. If the capability is available, IMS is registered and the service is * currently available over IMS. - * @see #isCapable(int) - * @throws ImsException if the IMS service is not available when calling this method - * {@link ImsRcsController#isAvailable(int, int)}. + * @see #isCapable(int, int) + * @throws ImsException if the IMS service is not available when calling this method. * See {@link ImsException#getCode()} for more information on the error codes. * @hide */ diff --git a/telephony/java/com/android/internal/telephony/SmsHeader.java b/telephony/java/com/android/internal/telephony/SmsHeader.java index ab3fdf4ebb41..2f3897b9bac2 100644 --- a/telephony/java/com/android/internal/telephony/SmsHeader.java +++ b/telephony/java/com/android/internal/telephony/SmsHeader.java @@ -23,6 +23,8 @@ import com.android.internal.util.HexDump; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; /** * SMS user data header, as specified in TS 23.040 9.2.3.24. @@ -71,6 +73,25 @@ public class SmsHeader { public static final int PORT_WAP_PUSH = 2948; public static final int PORT_WAP_WSP = 9200; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SmsHeader smsHeader = (SmsHeader) o; + return languageTable == smsHeader.languageTable + && languageShiftTable == smsHeader.languageShiftTable + && Objects.equals(portAddrs, smsHeader.portAddrs) + && Objects.equals(concatRef, smsHeader.concatRef) + && Objects.equals(specialSmsMsgList, smsHeader.specialSmsMsgList) + && Objects.equals(miscEltList, smsHeader.miscEltList); + } + + @Override + public int hashCode() { + return Objects.hash(portAddrs, concatRef, specialSmsMsgList, miscEltList, languageTable, + languageShiftTable); + } + public static class PortAddrs { @UnsupportedAppUsage public PortAddrs() { @@ -81,6 +102,21 @@ public class SmsHeader { @UnsupportedAppUsage public int origPort; public boolean areEightBits; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PortAddrs portAddrs = (PortAddrs) o; + return destPort == portAddrs.destPort + && origPort == portAddrs.origPort + && areEightBits == portAddrs.areEightBits; + } + + @Override + public int hashCode() { + return Objects.hash(destPort, origPort, areEightBits); + } } public static class ConcatRef { @@ -95,11 +131,41 @@ public class SmsHeader { @UnsupportedAppUsage public int msgCount; public boolean isEightBits; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConcatRef concatRef = (ConcatRef) o; + return refNumber == concatRef.refNumber + && seqNumber == concatRef.seqNumber + && msgCount == concatRef.msgCount + && isEightBits == concatRef.isEightBits; + } + + @Override + public int hashCode() { + return Objects.hash(refNumber, seqNumber, msgCount, isEightBits); + } } public static class SpecialSmsMsg { public int msgIndType; public int msgCount; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SpecialSmsMsg that = (SpecialSmsMsg) o; + return msgIndType == that.msgIndType + && msgCount == that.msgCount; + } + + @Override + public int hashCode() { + return Objects.hash(msgIndType, msgCount); + } } /** @@ -109,6 +175,22 @@ public class SmsHeader { public static class MiscElt { public int id; public byte[] data; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MiscElt miscElt = (MiscElt) o; + return id == miscElt.id + && Arrays.equals(data, miscElt.data); + } + + @Override + public int hashCode() { + int result = Objects.hash(id); + result = 31 * result + Arrays.hashCode(data); + return result; + } } @UnsupportedAppUsage diff --git a/tests/AutoVerify/app1/Android.bp b/tests/AutoVerify/app1/Android.bp new file mode 100644 index 000000000000..548519fa653b --- /dev/null +++ b/tests/AutoVerify/app1/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app1/AndroidManifest.xml b/tests/AutoVerify/app1/AndroidManifest.xml new file mode 100644 index 000000000000..d9caad490d82 --- /dev/null +++ b/tests/AutoVerify/app1/AndroidManifest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <intent-filter android:autoVerify="true"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app1/res/values/strings.xml b/tests/AutoVerify/app1/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app1/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<resources> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/AutoVerify/app2/Android.bp b/tests/AutoVerify/app2/Android.bp new file mode 100644 index 000000000000..1c6c97bdf350 --- /dev/null +++ b/tests/AutoVerify/app2/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest2", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app2/AndroidManifest.xml b/tests/AutoVerify/app2/AndroidManifest.xml new file mode 100644 index 000000000000..a00807883cfc --- /dev/null +++ b/tests/AutoVerify/app2/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <intent-filter android:autoVerify="true"> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + <data android:host="*.wildcard.tld" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app2/res/values/strings.xml b/tests/AutoVerify/app2/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app2/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<resources> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/AutoVerify/app3/Android.bp b/tests/AutoVerify/app3/Android.bp new file mode 100644 index 000000000000..70a2b77d1000 --- /dev/null +++ b/tests/AutoVerify/app3/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest3", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app3/AndroidManifest.xml b/tests/AutoVerify/app3/AndroidManifest.xml new file mode 100644 index 000000000000..efaabc9a38d3 --- /dev/null +++ b/tests/AutoVerify/app3/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <!-- does not request autoVerify --> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app3/res/values/strings.xml b/tests/AutoVerify/app3/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app3/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<resources> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/tests/AutoVerify/app4/Android.bp b/tests/AutoVerify/app4/Android.bp new file mode 100644 index 000000000000..fbdae1181a7a --- /dev/null +++ b/tests/AutoVerify/app4/Android.bp @@ -0,0 +1,11 @@ +android_app { + name: "AutoVerifyTest4", + srcs: ["src/**/*.java"], + resource_dirs: ["res"], + platform_apis: true, + min_sdk_version: "26", + target_sdk_version: "26", + optimize: { + enabled: false, + }, +} diff --git a/tests/AutoVerify/app4/AndroidManifest.xml b/tests/AutoVerify/app4/AndroidManifest.xml new file mode 100644 index 000000000000..1c975f8336c9 --- /dev/null +++ b/tests/AutoVerify/app4/AndroidManifest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.test.autoverify" > + + <uses-sdk android:targetSdkVersion="26" /> + + <application + android:label="@string/app_name" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <!-- intentionally does not autoVerify --> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + <data android:scheme="http" /> + <data android:scheme="https" /> + <data android:host="explicit.example.com" /> + <data android:host="*.wildcard.tld" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/AutoVerify/app4/res/values/strings.xml b/tests/AutoVerify/app4/res/values/strings.xml new file mode 100644 index 000000000000..e234355041c6 --- /dev/null +++ b/tests/AutoVerify/app4/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 2020 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<resources> + <!-- app icon label, do not translate --> + <string name="app_name" translatable="false">AutoVerify Test</string> +</resources> diff --git a/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java new file mode 100644 index 000000000000..09ef47212622 --- /dev/null +++ b/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2020 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. + */ diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java index 6c8dc00cb579..7d20d0d09dc2 100644 --- a/wifi/java/android/net/wifi/WifiManager.java +++ b/wifi/java/android/net/wifi/WifiManager.java @@ -758,6 +758,13 @@ public class WifiManager { @SystemApi public static final int SAP_CLIENT_BLOCK_REASON_CODE_NO_MORE_STAS = 1; + /** + * Client disconnected for unspecified reason. This could for example be because the AP is being + * shut down. + * @hide + */ + public static final int SAP_CLIENT_DISCONNECT_REASON_CODE_UNSPECIFIED = 2; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"IFACE_IP_MODE_"}, value = { |