diff options
122 files changed, 3994 insertions, 2096 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index 5dc7929d06df..181932cf1260 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -725,7 +725,6 @@ package android.content.pm { field public static final int FLAG_PERMISSION_USER_SET = 1; // 0x1 field public static final int MATCH_FACTORY_ONLY = 2097152; // 0x200000 field public static final int MATCH_KNOWN_PACKAGES = 4202496; // 0x402000 - field public static boolean RESTRICTED_PERMISSIONS_ENABLED; field public static final String SYSTEM_SHARED_LIBRARY_SERVICES = "android.ext.services"; field public static final String SYSTEM_SHARED_LIBRARY_SHARED = "android.ext.shared"; } diff --git a/cmds/incidentd/Android.bp b/cmds/incidentd/Android.bp index 534a38f14136..25e0328b4f38 100644 --- a/cmds/incidentd/Android.bp +++ b/cmds/incidentd/Android.bp @@ -60,12 +60,6 @@ cc_binary { "libservices", "libutils", "libprotobuf-cpp-lite", - "libcrypto", - "libkeystore_aidl", - "libkeystore_binder", - "libkeystore_parcelables", - "android.hardware.keymaster@4.0", - "libkeymaster4support", ], static_libs: [ @@ -119,8 +113,6 @@ cc_test { "src/incidentd_util.cpp", "src/proto_util.cpp", "src/report_directory.cpp", - "src/cipher/IncidentKeyStore.cpp", - "src/cipher/ProtoEncryption.cpp", "src/**/*.proto", ], @@ -142,12 +134,6 @@ cc_test { "libprotoutil", "libservices", "libutils", - "libcrypto", - "libkeystore_aidl", - "libkeystore_binder", - "libkeystore_parcelables", - "android.hardware.keymaster@4.0", - "libkeymaster4support", ], target: { diff --git a/cmds/incidentd/src/Privacy.cpp b/cmds/incidentd/src/Privacy.cpp index 0cc358fd2746..0a187e166135 100644 --- a/cmds/incidentd/src/Privacy.cpp +++ b/cmds/incidentd/src/Privacy.cpp @@ -28,8 +28,6 @@ namespace incidentd { using namespace android::os; using std::strstream; -static const bool kEncryptionEnabled = false; - uint64_t encode_field_id(const Privacy* p) { return (uint64_t)p->type << 32 | p->field_id; } string Privacy::toString() const { @@ -52,10 +50,6 @@ const Privacy* lookup(const Privacy* p, uint32_t fieldId) { return NULL; } -bool sectionEncryption(int section_id) { - return kEncryptionEnabled ? (section_id == 3025) /*restricted image section*/ : false; -} - static bool isAllowed(const uint8_t policy, const uint8_t check) { switch (check) { case PRIVACY_POLICY_LOCAL: diff --git a/cmds/incidentd/src/Privacy.h b/cmds/incidentd/src/Privacy.h index 9cde748bc44c..763edb03e485 100644 --- a/cmds/incidentd/src/Privacy.h +++ b/cmds/incidentd/src/Privacy.h @@ -90,9 +90,6 @@ private: uint8_t mPolicy; }; -// TODO: Add privacy flag in incident.proto and auto generate it inside Privacy. -bool sectionEncryption(int section_id); - /** * If a privacy policy is other than the defined values, update it to a real one. */ diff --git a/cmds/incidentd/src/PrivacyFilter.cpp b/cmds/incidentd/src/PrivacyFilter.cpp index ca6fb3708ef1..d00ecdde5c63 100644 --- a/cmds/incidentd/src/PrivacyFilter.cpp +++ b/cmds/incidentd/src/PrivacyFilter.cpp @@ -16,20 +16,19 @@ #define DEBUG false #include "Log.h" +#include "incidentd_util.h" #include "PrivacyFilter.h" +#include "proto_util.h" #include "incidentd_util.h" #include "proto_util.h" #include "Section.h" #include <android-base/file.h> -#include <android/util/ProtoFileReader.h> #include <android/util/protobuf.h> +#include <android/util/ProtoFileReader.h> #include <log/log.h> -#include "cipher/IncidentKeyStore.h" -#include "cipher/ProtoEncryption.h" - namespace android { namespace os { namespace incidentd { @@ -146,8 +145,6 @@ public: */ status_t writeData(int fd); - sp<ProtoReader> getData() { return mData; } - private: /** * The global set of field --> required privacy level mapping. @@ -259,47 +256,8 @@ void PrivacyFilter::addFd(const sp<FilterFd>& output) { mOutputs.push_back(output); } -static void write_section_to_file(int sectionId, FieldStripper& fieldStripper, sp<FilterFd> output, - bool encryptIfNeeded) { - status_t err; - - if (sectionEncryption(sectionId) && encryptIfNeeded) { - ProtoEncryptor encryptor(fieldStripper.getData()); - size_t encryptedSize = encryptor.encrypt(); - - if (encryptedSize <= 0) { - output->onWriteError(BAD_VALUE); - return; - } - err = write_section_header(output->getFd(), sectionId, encryptedSize); - VLOG("Encrypted: write section header size %lu", (unsigned long)encryptedSize); - - encryptor.flush(output->getFd()); - - if (err != NO_ERROR) { - output->onWriteError(err); - return; - } - } else { - err = write_section_header(output->getFd(), sectionId, fieldStripper.dataSize()); - VLOG("No encryption: write section header size %lu", - (unsigned long)fieldStripper.dataSize()); - - if (err != NO_ERROR) { - output->onWriteError(err); - return; - } - - err = fieldStripper.writeData(output->getFd()); - if (err != NO_ERROR) { - output->onWriteError(err); - return; - } - } -} - -status_t PrivacyFilter::writeData(const FdBuffer& buffer, uint8_t bufferLevel, size_t* maxSize, - bool encryptIfNeeded) { +status_t PrivacyFilter::writeData(const FdBuffer& buffer, uint8_t bufferLevel, + size_t* maxSize) { status_t err; if (maxSize != NULL) { @@ -309,9 +267,9 @@ status_t PrivacyFilter::writeData(const FdBuffer& buffer, uint8_t bufferLevel, s // Order the writes by privacy filter, with increasing levels of filtration,k // so we can do the filter once, and then write many times. sort(mOutputs.begin(), mOutputs.end(), - [](const sp<FilterFd>& a, const sp<FilterFd>& b) -> bool { - return a->getPrivacyPolicy() < b->getPrivacyPolicy(); - }); + [](const sp<FilterFd>& a, const sp<FilterFd>& b) -> bool { + return a->getPrivacyPolicy() < b->getPrivacyPolicy(); + }); uint8_t privacyPolicy = PRIVACY_POLICY_LOCAL; // a.k.a. no filtering FieldStripper fieldStripper(mRestrictions, buffer.data()->read(), bufferLevel); @@ -330,7 +288,17 @@ status_t PrivacyFilter::writeData(const FdBuffer& buffer, uint8_t bufferLevel, s // Write the resultant buffer to the fd, along with the header. ssize_t dataSize = fieldStripper.dataSize(); if (dataSize > 0) { - write_section_to_file(mSectionId, fieldStripper, output, encryptIfNeeded); + err = write_section_header(output->getFd(), mSectionId, dataSize); + if (err != NO_ERROR) { + output->onWriteError(err); + continue; + } + + err = fieldStripper.writeData(output->getFd()); + if (err != NO_ERROR) { + output->onWriteError(err); + continue; + } } if (maxSize != NULL) { @@ -382,18 +350,8 @@ status_t filter_and_write_report(int to, int from, uint8_t bufferLevel, // Read this section from the reader into an FdBuffer size_t sectionSize = reader->readRawVarint(); - FdBuffer sectionData; - - // Write data to FdBuffer, if the section was encrypted, decrypt first. - if (sectionEncryption(fieldId)) { - VLOG("sectionSize %lu", (unsigned long)sectionSize); - ProtoDecryptor decryptor(reader, sectionSize); - err = decryptor.decryptAndFlush(§ionData); - } else { - err = sectionData.write(reader, sectionSize); - } - + err = sectionData.write(reader, sectionSize); if (err != NO_ERROR) { ALOGW("filter_and_write_report FdBuffer.write failed (this shouldn't happen): %s", strerror(-err)); @@ -401,8 +359,7 @@ status_t filter_and_write_report(int to, int from, uint8_t bufferLevel, } // Do the filter and write. - err = filter.writeData(sectionData, bufferLevel, nullptr, - false /* do not encrypt again*/); + err = filter.writeData(sectionData, bufferLevel, nullptr); if (err != NO_ERROR) { ALOGW("filter_and_write_report filter.writeData had an error: %s", strerror(-err)); return err; @@ -411,7 +368,6 @@ status_t filter_and_write_report(int to, int from, uint8_t bufferLevel, // We don't need this field. Incident does not have any direct children // other than sections. So just skip them. write_field_or_skip(NULL, reader, fieldTag, true); - VLOG("Skip this.... section %d", fieldId); } } diff --git a/cmds/incidentd/src/PrivacyFilter.h b/cmds/incidentd/src/PrivacyFilter.h index d426db966a9a..76b28498a0ac 100644 --- a/cmds/incidentd/src/PrivacyFilter.h +++ b/cmds/incidentd/src/PrivacyFilter.h @@ -82,14 +82,8 @@ public: * was written (i.e. after filtering). * * The buffer is assumed to have already been filtered to bufferLevel. - * - * This function can be called when persisting data to disk or when sending - * data to client. In the former case, we need to encrypt the data when that - * section requires encryption. In the latter case, we shouldn't send the - * unencrypted data to client. */ - status_t writeData(const FdBuffer& buffer, uint8_t bufferLevel, size_t* maxSize, - bool encryptIfNeeded); + status_t writeData(const FdBuffer& buffer, uint8_t bufferLevel, size_t* maxSize); private: int mSectionId; diff --git a/cmds/incidentd/src/Reporter.cpp b/cmds/incidentd/src/Reporter.cpp index dc4065bce39b..02b6bbe6c9b1 100644 --- a/cmds/incidentd/src/Reporter.cpp +++ b/cmds/incidentd/src/Reporter.cpp @@ -465,8 +465,7 @@ status_t ReportWriter::writeSection(const FdBuffer& buffer) { } }); - return filter.writeData(buffer, PRIVACY_POLICY_LOCAL, &mMaxSectionDataFilteredSize, - true /*encrypt if needed*/); + return filter.writeData(buffer, PRIVACY_POLICY_LOCAL, &mMaxSectionDataFilteredSize); } diff --git a/cmds/incidentd/src/cipher/IncidentKeyStore.cpp b/cmds/incidentd/src/cipher/IncidentKeyStore.cpp deleted file mode 100644 index ae0a92094d0b..000000000000 --- a/cmds/incidentd/src/cipher/IncidentKeyStore.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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. - */ - -#include "Log.h" - -#include "IncidentKeyStore.h" - -#include <sys/stat.h> - -static constexpr size_t AES_KEY_BYTES = 32; -static constexpr size_t GCM_MAC_BYTES = 16; -constexpr char kKeyname[] = "IncidentKey"; - -namespace android { -namespace os { -namespace incidentd { - -using namespace keystore; -using std::string; - -IncidentKeyStore& IncidentKeyStore::getInstance() { - static IncidentKeyStore sInstance(new keystore::KeystoreClientImpl); - return sInstance; -} - -bool IncidentKeyStore::encrypt(const string& data, int32_t flags, string* output) { - std::lock_guard<std::mutex> lock(mMutex); - if (data.empty()) { - ALOGW("IncidentKeyStore: Encrypt empty data?!"); - return false; - } - if (!mClient->doesKeyExist(kKeyname)) { - auto gen_result = generateKeyLocked(kKeyname, 0); - if (!gen_result.isOk()) { - ALOGE("IncidentKeyStore: Key generate failed."); - return false; - } - } - if (!mClient->encryptWithAuthentication(kKeyname, data, flags, output)) { - ALOGE("IncidentKeyStore: Encryption failed."); - return false; - } - return true; -} - -bool IncidentKeyStore::decrypt(const std::string& input, string* output) { - std::lock_guard<std::mutex> lock(mMutex); - if (input.empty()) { - ALOGE("IncidentKeyStore: Decrypt empty input?"); - return false; - } - if (!mClient->decryptWithAuthentication(kKeyname, input, output)) { - ALOGE("IncidentKeyStore: Decryption failed."); - return false; - } - return true; -} - -KeyStoreNativeReturnCode IncidentKeyStore::generateKeyLocked(const std::string& name, - int32_t flags) { - auto paramBuilder = AuthorizationSetBuilder() - .AesEncryptionKey(AES_KEY_BYTES * 8) - .GcmModeMinMacLen(GCM_MAC_BYTES * 8) - .Authorization(TAG_NO_AUTH_REQUIRED); - - AuthorizationSet hardware_enforced_characteristics; - AuthorizationSet software_enforced_characteristics; - return mClient->generateKey(name, paramBuilder, flags, &hardware_enforced_characteristics, - &software_enforced_characteristics); -} - -} // namespace incidentd -} // namespace os -} // namespace android diff --git a/cmds/incidentd/src/cipher/IncidentKeyStore.h b/cmds/incidentd/src/cipher/IncidentKeyStore.h deleted file mode 100644 index 27611cd7faad..000000000000 --- a/cmds/incidentd/src/cipher/IncidentKeyStore.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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. - */ -#pragma once - -#include <keystore/keystore_client_impl.h> - -namespace android { -namespace os { -namespace incidentd { - -class IncidentKeyStore { -public: - static IncidentKeyStore& getInstance(); - - IncidentKeyStore(keystore::KeystoreClient* client) : mClient(client) {} - - /** - * Encrypt the plainText and output the encrypted message. - * - * Returns true on success and false otherwise. - * If the key has not been created yet, it will generate the key in KeyMaster. - */ - bool encrypt(const std::string& plainText, int32_t flags, std::string* output); - - /** - * Decrypt and output the decrypted message. - * - * Returns true on success and false otherwise. - */ - bool decrypt(const std::string& encryptedData, std::string* output); - -private: - std::unique_ptr<keystore::KeystoreClient> mClient; - std::mutex mMutex; - keystore::KeyStoreNativeReturnCode generateKeyLocked(const std::string& name, int32_t flags); -}; - -} // namespace incidentd -} // namespace os -} // namespace android diff --git a/cmds/incidentd/src/cipher/ProtoEncryption.cpp b/cmds/incidentd/src/cipher/ProtoEncryption.cpp deleted file mode 100644 index 493796d2af4d..000000000000 --- a/cmds/incidentd/src/cipher/ProtoEncryption.cpp +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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. - */ - -#define DEBUG true // STOPSHIP if true -#include "Log.h" - -#include "ProtoEncryption.h" - -#include <android/util/protobuf.h> - -#include "IncidentKeyStore.h" - -namespace android { -namespace os { -namespace incidentd { - -using android::util::FIELD_COUNT_REPEATED; -using android::util::FIELD_TYPE_STRING; -using android::util::ProtoOutputStream; -using android::util::ProtoReader; -using std::string; - -static const int FIELD_ID_BLOCK = 1; - -size_t ProtoEncryptor::encrypt() { - string block; - int i = 0; - // Read at most sBlockSize at a time and encrypt. - while (mReader->readBuffer() != NULL) { - size_t readBytes = - mReader->currentToRead() > sBlockSize ? sBlockSize : mReader->currentToRead(); - block.resize(readBytes); - std::memcpy(block.data(), mReader->readBuffer(), readBytes); - - string encrypted; - if (IncidentKeyStore::getInstance().encrypt(block, 0, &encrypted)) { - mOutputStream.write(FIELD_TYPE_STRING | FIELD_ID_BLOCK | FIELD_COUNT_REPEATED, - encrypted); - VLOG("Block %d Encryption: original %lld now %lld", i++, (long long)readBytes, - (long long)encrypted.length()); - mReader->move(readBytes); - } else { - return 0; - } - } - return mOutputStream.size(); -} - -status_t ProtoEncryptor::flush(int fd) { - if (!mOutputStream.flush(fd)) { - return BAD_VALUE; - } - return NO_ERROR; -} - -status_t ProtoDecryptor::readOneBlock(string* output) { - if (!mReader->hasNext()) { - return NO_ERROR; - } - uint64_t fieldTag = mReader->readRawVarint(); - uint32_t fieldId = read_field_id(fieldTag); - uint8_t wireType = read_wire_type(fieldTag); - if (wireType == WIRE_TYPE_LENGTH_DELIMITED) { - // Read this section from the reader into an FdBuffer - size_t sectionSize = mReader->readRawVarint(); - output->resize(sectionSize); - size_t pos = 0; - while (pos < sectionSize && mReader->readBuffer() != NULL) { - size_t toRead = (sectionSize - pos) > mReader->currentToRead() - ? mReader->currentToRead() - : (sectionSize - pos); - std::memcpy(&((output->data())[pos]), mReader->readBuffer(), toRead); - pos += toRead; - mReader->move(toRead); - } - if (pos != sectionSize) { - return BAD_VALUE; - ALOGE("Failed to read one block"); - } - } else { - return BAD_VALUE; - } - return NO_ERROR; -} - -status_t ProtoDecryptor::decryptAndFlush(FdBuffer* out) { - size_t mStartBytes = mReader->bytesRead(); - size_t bytesRead = 0; - int i = 0; - status_t err = NO_ERROR; - // Let's read until we read mTotalSize. If any error occurs before that, make sure to move the - // read pointer so the caller can continue to read the following sections. - while (bytesRead < mTotalSize) { - string block; - err = readOneBlock(&block); - bytesRead = mReader->bytesRead() - mStartBytes; - - if (err != NO_ERROR) { - break; - } - - if (block.length() == 0) { - VLOG("Done reading all blocks"); - break; - } - - string decryptedBlock; - if ((IncidentKeyStore::getInstance()).decrypt(block, &decryptedBlock)) { - VLOG("Block %d Original Size %lu Decrypted size %lu", i++, - (unsigned long)block.length(), (unsigned long)decryptedBlock.length()); - out->write(reinterpret_cast<uint8_t*>(decryptedBlock.data()), decryptedBlock.length()); - } else { - err = BAD_VALUE; - break; - } - } - - if (bytesRead < mTotalSize) { - mReader->move(mTotalSize - bytesRead); - } - return err; -} - -} // namespace incidentd -} // namespace os -} // namespace android diff --git a/cmds/incidentd/src/cipher/ProtoEncryption.h b/cmds/incidentd/src/cipher/ProtoEncryption.h deleted file mode 100644 index 5b72ca88ec64..000000000000 --- a/cmds/incidentd/src/cipher/ProtoEncryption.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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. - */ -#pragma once - -#include <android/util/ProtoOutputStream.h> -#include <android/util/ProtoReader.h> -#include <frameworks/base/cmds/incidentd/src/cipher/cipher_blocks.pb.h> - -#include "FdBuffer.h" - -namespace android { -namespace os { -namespace incidentd { - -// PlainText IncidentReport format -// [section1_header(id, size, type)][section1_data] ... - -// Let's say section1 needs encryption -// After encryption, it becomes -// [section1_header(id, encrypted_size, type)][[cipher_block][cipher_block][cipher_block]..] - -// When clients read the report, it's decrypted, and written in its original format - -/** - * Takes a ProtoReader, encrypts its whole content -- which is one section, and flush to - * a file descriptor. - * The underlying encryption is done using Keystore binder APIs. We encrypt the data - * in blocks, and write to the file in android.os.incidentd.CipherBlocks format. - */ -class ProtoEncryptor { -public: - ProtoEncryptor(const sp<android::util::ProtoReader>& reader) : mReader(reader){}; - - // Encrypt the data from ProtoReader, and store in CipherBlocks format. - // return the size of CipherBlocks. - size_t encrypt(); - - status_t flush(int fd); - -private: - static const size_t sBlockSize = 8 * 1024; - const sp<android::util::ProtoReader> mReader; - android::util::ProtoOutputStream mOutputStream; -}; - -// Read data from ProtoReader, which is in CipherBlocks proto format. Parse and decrypt -// block by block. -class ProtoDecryptor { -public: - ProtoDecryptor(const sp<android::util::ProtoReader>& reader, size_t size) - : mReader(reader), mTotalSize(size){}; - status_t decryptAndFlush(FdBuffer* out); - -private: - const sp<android::util::ProtoReader> mReader; - - // Total size in bytes we should read from ProtoReader. - const size_t mTotalSize; - - // Read one cipher block from ProtoReader, instead of reading the whole content - // and parse to CipherBlocks which could be huge. - status_t readOneBlock(std::string* output); -}; - -} // namespace incidentd -} // namespace os -} // namespace android diff --git a/cmds/incidentd/src/cipher/cipher_blocks.proto b/cmds/incidentd/src/cipher/cipher_blocks.proto deleted file mode 100644 index 5c7ed242c7a5..000000000000 --- a/cmds/incidentd/src/cipher/cipher_blocks.proto +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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. - */ - -syntax = "proto2"; - -package android.os.incidentd; - -// This proto is never instantiated anywhere. It only exists to keep a record of the format of the -// encrypted data on disk. -message CipherBlocks { - repeated string blocks = 1; -} diff --git a/cmds/incidentd/tests/IncidentKeyStore_test.cpp b/cmds/incidentd/tests/IncidentKeyStore_test.cpp deleted file mode 100644 index 2250fda90e01..000000000000 --- a/cmds/incidentd/tests/IncidentKeyStore_test.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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. - */ - -#include "cipher/IncidentKeyStore.h" - -#include <binder/ProcessState.h> -#include <gtest/gtest.h> - -#include <fstream> - -using namespace android::os::incidentd; - -class IncidentKeyStoreTest : public ::testing::Test { -protected: - std::unique_ptr<IncidentKeyStore> incidentKeyStore; - void SetUp() override { - android::ProcessState::self()->startThreadPool(); - incidentKeyStore = std::make_unique<IncidentKeyStore>( - static_cast<keystore::KeystoreClient*>(new keystore::KeystoreClientImpl)); - }; - void TearDown() override { incidentKeyStore = nullptr; }; -}; - -TEST_F(IncidentKeyStoreTest, test_encrypt_decrypt) { - std::string plaintext; - plaintext.resize(4 * 1024, 'a'); - - std::string encrypted; - EXPECT_TRUE(incidentKeyStore->encrypt(plaintext, 0, &encrypted)); - std::string decrypted; - EXPECT_TRUE(incidentKeyStore->decrypt(encrypted, &decrypted)); - - EXPECT_FALSE(encrypted.empty()); - EXPECT_EQ(plaintext, decrypted); -} - -TEST_F(IncidentKeyStoreTest, test_encrypt_empty_hash) { - std::string hash = ""; - - std::string encrypted; - EXPECT_FALSE(incidentKeyStore->encrypt(hash, 0, &encrypted)); - - EXPECT_TRUE(encrypted.empty()); -} - -TEST_F(IncidentKeyStoreTest, test_decrypt_empty_hash) { - std::string hash = ""; - - std::string decrypted; - EXPECT_FALSE(incidentKeyStore->decrypt(hash, &decrypted)); - - EXPECT_TRUE(decrypted.empty()); -}
\ No newline at end of file diff --git a/cmds/incidentd/tests/ProtoEncryption_test.cpp b/cmds/incidentd/tests/ProtoEncryption_test.cpp deleted file mode 100644 index 6742e034d70d..000000000000 --- a/cmds/incidentd/tests/ProtoEncryption_test.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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. - */ - -#include "Log.h" - -#include "cipher/ProtoEncryption.h" - -#include <android-base/file.h> -#include <gtest/gtest.h> - -#include "FdBuffer.h" -#include "android/util/ProtoFileReader.h" - -using namespace android::os::incidentd; -using android::sp; -using std::string; -using ::testing::Test; - -const std::string kTestPath = GetExecutableDirectory(); -const std::string kTestDataPath = kTestPath + "/testdata/"; - -TEST(ProtoEncryptionTest, test_encrypt_decrypt) { - const std::string plaintextFile = kTestDataPath + "plaintext.txt"; - const std::string encryptedFile = kTestDataPath + "encrypted.txt"; - size_t msg1Size = 20 * 1024; - - // Create a file with plain text. - { - unique_fd fd( - open(plaintextFile.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC, S_IRUSR | S_IWUSR)); - ASSERT_NE(fd.get(), -1); - string content; - content.resize(msg1Size, 'a'); - WriteFully(fd, content.data(), msg1Size); - } - - // Read the plain text and encrypted - { - unique_fd readFd(open(plaintextFile.c_str(), O_RDONLY | O_CLOEXEC)); - unique_fd encryptedFd( - open(encryptedFile.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC, S_IRUSR | S_IWUSR)); - - ASSERT_NE(readFd.get(), -1); - ASSERT_NE(encryptedFd.get(), -1); - - sp<ProtoFileReader> reader = new ProtoFileReader(readFd.get()); - ProtoEncryptor encryptor(reader); - EXPECT_TRUE(encryptor.encrypt() > msg1Size); - - encryptor.flush(encryptedFd.get()); - } - - // Read the encrypted file, and decrypt - unique_fd encryptedFd(open(encryptedFile.c_str(), O_RDONLY | O_CLOEXEC)); - ASSERT_NE(encryptedFd.get(), -1); - FdBuffer output; - sp<ProtoFileReader> reader2 = new ProtoFileReader(encryptedFd.get()); - ProtoDecryptor decryptor(reader2, reader2->size()); - decryptor.decryptAndFlush(&output); - - auto decryptedReader = output.data()->read(); - - // Check the content. - int count = 0; - while (decryptedReader->hasNext()) { - if (decryptedReader->next() == 'a') { - count++; - } - } - - EXPECT_EQ(msg1Size, count); -}
\ No newline at end of file diff --git a/config/hiddenapi-greylist-max-p.txt b/config/hiddenapi-greylist-max-p.txt index 4c643e1c6831..141e8e669dac 100644 --- a/config/hiddenapi-greylist-max-p.txt +++ b/config/hiddenapi-greylist-max-p.txt @@ -73,5 +73,5 @@ Lcom/android/internal/telephony/IPhoneSubInfo$Stub;-><init>()V Lcom/android/internal/telephony/ITelephonyRegistry;->notifyCallForwardingChanged(Z)V Lcom/android/internal/telephony/ITelephonyRegistry;->notifyCellLocation(Landroid/os/Bundle;)V Lcom/android/internal/telephony/ITelephonyRegistry;->notifyDataActivity(I)V -Lcom/android/internal/telephony/ITelephonyRegistry;->notifyOtaspChanged(I)V +Lcom/android/internal/telephony/ITelephonyRegistry;->notifyOtaspChanged(II)V Lcom/android/internal/view/BaseIWindow;-><init>()V diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 4b37461866d0..bfc8b1204373 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1069,7 +1069,7 @@ public final class ActivityThread extends ClientTransactionHandler { } public void scheduleApplicationInfoChanged(ApplicationInfo ai) { - mH.removeMessages(H.APPLICATION_INFO_CHANGED); + mH.removeMessages(H.APPLICATION_INFO_CHANGED, ai); sendMessage(H.APPLICATION_INFO_CHANGED, ai); } diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index a8815ec6cfaa..89eabc285e38 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -1278,7 +1278,7 @@ public class PackageInstaller { public int mode = MODE_INVALID; /** {@hide} */ @UnsupportedAppUsage - public int installFlags; + public int installFlags = PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS; /** {@hide} */ public int installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; /** {@hide} */ @@ -1513,18 +1513,21 @@ public class PackageInstaller { * state of the permission can be determined only at install time and cannot be * changed on updated or at a later point via the package manager APIs. * + * <p>Initially, all restricted permissions are whitelisted but you can change + * which ones are whitelisted by calling this method or the corresponding ones + * on the {@link PackageManager}. + * * @see PackageManager#addWhitelistedRestrictedPermission(String, String, int) * @see PackageManager#removeWhitelistedRestrictedPermission(String, String, int) */ public void setWhitelistedRestrictedPermissions(@Nullable Set<String> permissions) { if (permissions == RESTRICTED_PERMISSIONS_ALL) { installFlags |= PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS; - } - if (permissions != null) { - this.whitelistedRestrictedPermissions = new ArrayList<>(permissions); + whitelistedRestrictedPermissions = null; } else { installFlags &= ~PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS; - this.whitelistedRestrictedPermissions = null; + whitelistedRestrictedPermissions = (permissions != null) + ? new ArrayList<>(permissions) : null; } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index dd5ca6706316..40561f02d55f 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -86,11 +86,6 @@ public abstract class PackageManager { /** {@hide} */ public static final boolean APPLY_DEFAULT_TO_DEVICE_PROTECTED_STORAGE = true; - /** {@hide} */ - @TestApi - // STOPSHIP: Remove this once we get a Play prebuilt. - public static boolean RESTRICTED_PERMISSIONS_ENABLED = false; - /** * This exception is thrown when a given package, application, or component * name cannot be found. diff --git a/core/java/android/hardware/display/ColorDisplayManager.java b/core/java/android/hardware/display/ColorDisplayManager.java index ce71db630499..90d312e47bdc 100644 --- a/core/java/android/hardware/display/ColorDisplayManager.java +++ b/core/java/android/hardware/display/ColorDisplayManager.java @@ -162,6 +162,20 @@ public final class ColorDisplayManager { */ public static final int COLOR_MODE_AUTOMATIC = 3; + /** + * Display color mode range reserved for vendor customizations by the RenderIntent definition in + * hardware/interfaces/graphics/common/1.1/types.hal. These are NOT directly related to (but ARE + * mutually exclusive with) the {@link ColorMode} constants, but ARE directly related (and ARE + * mutually exclusive with) the DISPLAY_COLOR_* constants in DisplayTransformManager. + * + * @hide + */ + public static final int VENDOR_COLOR_MODE_RANGE_MIN = 256; // 0x100 + /** + * @hide + */ + public static final int VENDOR_COLOR_MODE_RANGE_MAX = 511; // 0x1ff + private final ColorDisplayManagerInternal mManager; private MetricsLogger mMetricsLogger; diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 0e10de8c4e3f..a69ca99500d6 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -3449,6 +3449,10 @@ public class ConnectivityManager { final NetworkCallback callback; synchronized (sCallbacks) { callback = sCallbacks.get(request); + if (message.what == CALLBACK_UNAVAIL) { + sCallbacks.remove(request); + callback.networkRequest = ALREADY_UNREGISTERED; + } } if (DBG) { Log.d(TAG, getCallbackName(message.what) + " for network " + network); @@ -3995,8 +3999,10 @@ public class ConnectivityManager { synchronized (sCallbacks) { Preconditions.checkArgument(networkCallback.networkRequest != null, "NetworkCallback was not registered"); - Preconditions.checkArgument(networkCallback.networkRequest != ALREADY_UNREGISTERED, - "NetworkCallback was already unregistered"); + if (networkCallback.networkRequest == ALREADY_UNREGISTERED) { + Log.d(TAG, "NetworkCallback was already unregistered"); + return; + } for (Map.Entry<NetworkRequest, NetworkCallback> e : sCallbacks.entrySet()) { if (e.getValue() == networkCallback) { reqs.add(e.getKey()); diff --git a/core/java/android/net/IpSecManager.java b/core/java/android/net/IpSecManager.java index 1145d5bd4d9a..83813da80c44 100644 --- a/core/java/android/net/IpSecManager.java +++ b/core/java/android/net/IpSecManager.java @@ -19,11 +19,13 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Binder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; @@ -749,6 +751,7 @@ public final class IpSecManager { * @hide */ @SystemApi + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void addAddress(@NonNull InetAddress address, int prefixLen) throws IOException { try { @@ -771,6 +774,7 @@ public final class IpSecManager { * @hide */ @SystemApi + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void removeAddress(@NonNull InetAddress address, int prefixLen) throws IOException { try { @@ -886,6 +890,7 @@ public final class IpSecManager { */ @SystemApi @NonNull + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public IpSecTunnelInterface createIpSecTunnelInterface(@NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork) @@ -916,6 +921,7 @@ public final class IpSecManager { * @hide */ @SystemApi + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public void applyTunnelModeTransform(@NonNull IpSecTunnelInterface tunnel, @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException { @@ -947,7 +953,8 @@ public final class IpSecManager { throw new IllegalArgumentException(sse); } else if (sse.errorCode == OsConstants.EAGAIN) { throw new IllegalStateException(sse); - } else if (sse.errorCode == OsConstants.EOPNOTSUPP) { + } else if (sse.errorCode == OsConstants.EOPNOTSUPP + || sse.errorCode == OsConstants.EPROTONOSUPPORT) { throw new UnsupportedOperationException(sse); } } diff --git a/core/java/android/net/IpSecTransform.java b/core/java/android/net/IpSecTransform.java index e519fdf65e50..36111f2a372d 100644 --- a/core/java/android/net/IpSecTransform.java +++ b/core/java/android/net/IpSecTransform.java @@ -21,9 +21,11 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -483,6 +485,7 @@ public final class IpSecTransform implements AutoCloseable { */ @SystemApi @NonNull + @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS) public IpSecTransform buildTunnelModeTransform( @NonNull InetAddress sourceAddress, diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java index 7dbc16a56a7b..1868d0596acc 100644 --- a/core/java/android/os/GraphicsEnvironment.java +++ b/core/java/android/os/GraphicsEnvironment.java @@ -618,28 +618,36 @@ public class GraphicsEnvironment { return false; } - final String anglePkgName = getAnglePackageName(pm); - if (anglePkgName.isEmpty()) { - Log.e(TAG, "Failed to find ANGLE package."); - return false; - } + ApplicationInfo angleInfo = null; - final ApplicationInfo angleInfo; - String angleDebugPackage = getAngleDebugPackage(context, bundle); - if (!angleDebugPackage.isEmpty()) { - Log.i(TAG, "ANGLE debug package enabled: " + angleDebugPackage); + // If the developer has specified a debug package over ADB, attempt to find it + String anglePkgName = getAngleDebugPackage(context, bundle); + if (!anglePkgName.isEmpty()) { + Log.i(TAG, "ANGLE debug package enabled: " + anglePkgName); try { // Note the debug package does not have to be pre-installed - angleInfo = pm.getApplicationInfo(angleDebugPackage, 0); + angleInfo = pm.getApplicationInfo(anglePkgName, 0); } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "ANGLE debug package '" + angleDebugPackage + "' not installed"); + Log.w(TAG, "ANGLE debug package '" + anglePkgName + "' not installed"); return false; } - } else { - try { - angleInfo = pm.getApplicationInfo(anglePkgName, PackageManager.MATCH_SYSTEM_ONLY); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "ANGLE package '" + anglePkgName + "' not installed"); + } + + // Otherwise, check to see if ANGLE is properly installed + if (angleInfo == null) { + anglePkgName = getAnglePackageName(pm); + if (!anglePkgName.isEmpty()) { + Log.i(TAG, "ANGLE package enabled: " + anglePkgName); + try { + // Production ANGLE libraries must be pre-installed as a system app + angleInfo = pm.getApplicationInfo(anglePkgName, + PackageManager.MATCH_SYSTEM_ONLY); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "ANGLE package '" + anglePkgName + "' not installed"); + return false; + } + } else { + Log.e(TAG, "Failed to find ANGLE package."); return false; } } @@ -700,7 +708,7 @@ public class GraphicsEnvironment { private boolean setupAndUseAngle(Context context, String packageName) { // Need to make sure we are evaluating ANGLE usage for the correct circumstances if (!setupAngle(context, null, context.getPackageManager(), packageName)) { - Log.v(TAG, "Package '" + packageName + "' should use not ANGLE"); + Log.v(TAG, "Package '" + packageName + "' should not use ANGLE"); return false; } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 7c5a1fb5f787..de888d3f9a7f 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3455,10 +3455,25 @@ public final class Settings { */ public static final String DISPLAY_COLOR_MODE = "display_color_mode"; - private static final Validator DISPLAY_COLOR_MODE_VALIDATOR = - new SettingsValidators.InclusiveIntegerRangeValidator( - ColorDisplayManager.COLOR_MODE_NATURAL, - ColorDisplayManager.COLOR_MODE_AUTOMATIC); + private static final Validator DISPLAY_COLOR_MODE_VALIDATOR = new Validator() { + @Override + public boolean validate(@Nullable String value) { + // Assume the actual validation that this device can properly handle this kind of + // color mode further down in ColorDisplayManager / ColorDisplayService. + try { + final int setting = Integer.parseInt(value); + final boolean isInFrameworkRange = + setting >= ColorDisplayManager.COLOR_MODE_NATURAL + && setting <= ColorDisplayManager.COLOR_MODE_AUTOMATIC; + final boolean isInVendorRange = + setting >= ColorDisplayManager.VENDOR_COLOR_MODE_RANGE_MIN + && setting <= ColorDisplayManager.VENDOR_COLOR_MODE_RANGE_MAX; + return isInFrameworkRange || isInVendorRange; + } catch (NumberFormatException | NullPointerException e) { + return false; + } + } + }; /** * The user selected peak refresh rate in frames per second. @@ -6484,6 +6499,21 @@ public final class Settings { new SettingsValidators.ComponentNameListValidator(":"); /** + * Whether the Global Actions Panel is enabled. + * @hide + */ + public static final String GLOBAL_ACTIONS_PANEL_ENABLED = "global_actions_panel_enabled"; + + private static final Validator GLOBAL_ACTIONS_PANEL_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR; + + /** + * Whether the Global Actions Panel can be toggled on or off in Settings. + * @hide + */ + public static final String GLOBAL_ACTIONS_PANEL_AVAILABLE = + "global_actions_panel_available"; + + /** * Whether the hush gesture has ever been used * @hide */ @@ -8243,6 +8273,16 @@ public final class Settings { BOOLEAN_VALIDATOR; /** + * Whether or not the face unlock education screen has been shown to the user. + * @hide + */ + public static final String FACE_UNLOCK_EDUCATION_INFO_DISPLAYED = + "face_unlock_education_info_displayed"; + + private static final Validator FACE_UNLOCK_EDUCATION_INFO_DISPLAYED_VALIDATOR = + BOOLEAN_VALIDATOR; + + /** * Whether or not debugging is enabled. * @hide */ @@ -8922,7 +8962,8 @@ public final class Settings { SILENCE_NOTIFICATION_GESTURE_COUNT, SILENCE_CALL_GESTURE_COUNT, SILENCE_TIMER_GESTURE_COUNT, - DARK_MODE_DIALOG_SEEN + DARK_MODE_DIALOG_SEEN, + GLOBAL_ACTIONS_PANEL_ENABLED }; /** @@ -9048,6 +9089,8 @@ public final class Settings { VALIDATORS.put(FACE_UNLOCK_APP_ENABLED, FACE_UNLOCK_APP_ENABLED_VALIDATOR); VALIDATORS.put(FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION_VALIDATOR); + VALIDATORS.put(FACE_UNLOCK_EDUCATION_INFO_DISPLAYED, + FACE_UNLOCK_EDUCATION_INFO_DISPLAYED_VALIDATOR); VALIDATORS.put(ASSIST_GESTURE_ENABLED, ASSIST_GESTURE_ENABLED_VALIDATOR); VALIDATORS.put(ASSIST_GESTURE_SILENCE_ALERTS_ENABLED, ASSIST_GESTURE_SILENCE_ALERTS_ENABLED_VALIDATOR); @@ -9106,6 +9149,7 @@ public final class Settings { VALIDATORS.put(ODI_CAPTIONS_ENABLED, ODI_CAPTIONS_ENABLED_VALIDATOR); VALIDATORS.put(DARK_MODE_DIALOG_SEEN, BOOLEAN_VALIDATOR); VALIDATORS.put(UI_NIGHT_MODE, UI_NIGHT_MODE_VALIDATOR); + VALIDATORS.put(GLOBAL_ACTIONS_PANEL_ENABLED, GLOBAL_ACTIONS_PANEL_ENABLED_VALIDATOR); } /** diff --git a/core/java/android/service/autofill/augmented/AugmentedAutofillService.java b/core/java/android/service/autofill/augmented/AugmentedAutofillService.java index 656127ad77a9..87e369f20d58 100644 --- a/core/java/android/service/autofill/augmented/AugmentedAutofillService.java +++ b/core/java/android/service/autofill/augmented/AugmentedAutofillService.java @@ -15,6 +15,9 @@ */ package android.service.autofill.augmented; +import static android.service.autofill.augmented.Helper.logResponse; +import static android.util.TimeUtils.formatDuration; + import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import android.annotation.CallSuper; @@ -38,9 +41,7 @@ import android.os.SystemClock; import android.service.autofill.augmented.PresentationParams.SystemPopupPresentationParams; import android.util.Log; import android.util.Pair; -import android.util.Slog; import android.util.SparseArray; -import android.util.TimeUtils; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; @@ -48,6 +49,7 @@ import android.view.autofill.IAugmentedAutofillManagerClient; import android.view.autofill.IAutofillWindowPresenter; import com.android.internal.annotations.GuardedBy; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -84,6 +86,9 @@ public abstract class AugmentedAutofillService extends Service { private SparseArray<AutofillProxy> mAutofillProxies; + // Used for metrics / debug only + private ComponentName mServiceComponentName; + private final IAugmentedAutofillService mInterface = new IAugmentedAutofillService.Stub() { @Override @@ -125,6 +130,7 @@ public abstract class AugmentedAutofillService extends Service { /** @hide */ @Override public final IBinder onBind(Intent intent) { + mServiceComponentName = intent.getComponent(); if (SERVICE_INTERFACE.equals(intent.getAction())) { return mInterface.asBinder(); } @@ -215,8 +221,9 @@ public abstract class AugmentedAutofillService extends Service { final CancellationSignal cancellationSignal = CancellationSignal.fromTransport(transport); AutofillProxy proxy = mAutofillProxies.get(sessionId); if (proxy == null) { - proxy = new AutofillProxy(sessionId, client, taskId, componentName, focusedId, - focusedValue, requestTime, callback, cancellationSignal); + proxy = new AutofillProxy(sessionId, client, taskId, mServiceComponentName, + componentName, focusedId, focusedValue, requestTime, callback, + cancellationSignal); mAutofillProxies.put(sessionId, proxy); } else { // TODO(b/123099468): figure out if it's ok to reuse the proxy; add logging @@ -272,6 +279,8 @@ public abstract class AugmentedAutofillService extends Service { @Override /** @hide */ protected final void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.print("Service component: "); pw.println( + ComponentName.flattenToShortString(mServiceComponentName)); if (mAutofillProxies != null) { final int size = mAutofillProxies.size(); pw.print("Number proxies: "); pw.println(size); @@ -301,12 +310,12 @@ public abstract class AugmentedAutofillService extends Service { /** @hide */ static final class AutofillProxy { - static final int REPORT_EVENT_ON_SUCCESS = 1; + static final int REPORT_EVENT_NO_RESPONSE = 1; static final int REPORT_EVENT_UI_SHOWN = 2; static final int REPORT_EVENT_UI_DESTROYED = 3; @IntDef(prefix = { "REPORT_EVENT_" }, value = { - REPORT_EVENT_ON_SUCCESS, + REPORT_EVENT_NO_RESPONSE, REPORT_EVENT_UI_SHOWN, REPORT_EVENT_UI_DESTROYED }) @@ -319,6 +328,8 @@ public abstract class AugmentedAutofillService extends Service { private final int mSessionId; public final int taskId; public final ComponentName componentName; + // Used for metrics / debug only + private String mServicePackageName; @GuardedBy("mLock") private AutofillId mFocusedId; @GuardedBy("mLock") @@ -349,6 +360,7 @@ public abstract class AugmentedAutofillService extends Service { private CancellationSignal mCancellationSignal; private AutofillProxy(int sessionId, @NonNull IBinder client, int taskId, + @NonNull ComponentName serviceComponentName, @NonNull ComponentName componentName, @NonNull AutofillId focusedId, @Nullable AutofillValue focusedValue, long requestTime, @NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal) { @@ -357,6 +369,7 @@ public abstract class AugmentedAutofillService extends Service { mCallback = callback; this.taskId = taskId; this.componentName = componentName; + mServicePackageName = serviceComponentName.getPackageName(); mFocusedId = focusedId; mFocusedValue = focusedValue; mFirstRequestTime = requestTime; @@ -439,9 +452,9 @@ public abstract class AugmentedAutofillService extends Service { mCallback.cancel(); } } catch (RemoteException e) { - Slog.e(TAG, "failed to check current pending request status", e); + Log.e(TAG, "failed to check current pending request status", e); } - Slog.d(TAG, "mCallback is updated."); + Log.d(TAG, "mCallback is updated."); } mCallback = callback; } @@ -463,13 +476,17 @@ public abstract class AugmentedAutofillService extends Service { // Used (mostly) for metrics. public void report(@ReportEvent int event) { + if (sVerbose) Log.v(TAG, "report(): " + event); + long duration = -1; + int type = MetricsEvent.TYPE_UNKNOWN; switch (event) { - case REPORT_EVENT_ON_SUCCESS: + case REPORT_EVENT_NO_RESPONSE: + type = MetricsEvent.TYPE_SUCCESS; if (mFirstOnSuccessTime == 0) { mFirstOnSuccessTime = SystemClock.elapsedRealtime(); + duration = mFirstOnSuccessTime - mFirstRequestTime; if (sDebug) { - Slog.d(TAG, "Service responded in " + TimeUtils.formatDuration( - mFirstOnSuccessTime - mFirstRequestTime)); + Log.d(TAG, "Service responded nothing in " + formatDuration(duration)); } } try { @@ -479,27 +496,25 @@ public abstract class AugmentedAutofillService extends Service { } break; case REPORT_EVENT_UI_SHOWN: + type = MetricsEvent.TYPE_OPEN; if (mUiFirstShownTime == 0) { mUiFirstShownTime = SystemClock.elapsedRealtime(); - if (sDebug) { - Slog.d(TAG, "UI shown in " + TimeUtils.formatDuration( - mUiFirstShownTime - mFirstRequestTime)); - } + duration = mUiFirstShownTime - mFirstRequestTime; + if (sDebug) Log.d(TAG, "UI shown in " + formatDuration(duration)); } break; case REPORT_EVENT_UI_DESTROYED: + type = MetricsEvent.TYPE_CLOSE; if (mUiFirstDestroyedTime == 0) { mUiFirstDestroyedTime = SystemClock.elapsedRealtime(); - if (sDebug) { - Slog.d(TAG, "UI destroyed in " + TimeUtils.formatDuration( - mUiFirstDestroyedTime - mFirstRequestTime)); - } + duration = mUiFirstDestroyedTime - mFirstRequestTime; + if (sDebug) Log.d(TAG, "UI destroyed in " + formatDuration(duration)); } break; default: - Slog.w(TAG, "invalid event reported: " + event); + Log.w(TAG, "invalid event reported: " + event); } - // TODO(b/122858578): log metrics as well + logResponse(type, mServicePackageName, componentName, mSessionId, duration); } public void dump(@NonNull String prefix, @NonNull PrintWriter pw) { @@ -527,19 +542,19 @@ public abstract class AugmentedAutofillService extends Service { if (mFirstOnSuccessTime > 0) { final long responseTime = mFirstOnSuccessTime - mFirstRequestTime; pw.print(prefix); pw.print("response time: "); - TimeUtils.formatDuration(responseTime, pw); pw.println(); + formatDuration(responseTime, pw); pw.println(); } if (mUiFirstShownTime > 0) { final long uiRenderingTime = mUiFirstShownTime - mFirstRequestTime; pw.print(prefix); pw.print("UI rendering time: "); - TimeUtils.formatDuration(uiRenderingTime, pw); pw.println(); + formatDuration(uiRenderingTime, pw); pw.println(); } if (mUiFirstDestroyedTime > 0) { final long uiTotalTime = mUiFirstDestroyedTime - mFirstRequestTime; pw.print(prefix); pw.print("UI life time: "); - TimeUtils.formatDuration(uiTotalTime, pw); pw.println(); + formatDuration(uiTotalTime, pw); pw.println(); } } diff --git a/core/java/android/service/autofill/augmented/FillCallback.java b/core/java/android/service/autofill/augmented/FillCallback.java index 33e6a8c25ba4..0251386bd7ce 100644 --- a/core/java/android/service/autofill/augmented/FillCallback.java +++ b/core/java/android/service/autofill/augmented/FillCallback.java @@ -50,8 +50,10 @@ public final class FillCallback { public void onSuccess(@Nullable FillResponse response) { if (sDebug) Log.d(TAG, "onSuccess(): " + response); - mProxy.report(AutofillProxy.REPORT_EVENT_ON_SUCCESS); - if (response == null) return; + if (response == null) { + mProxy.report(AutofillProxy.REPORT_EVENT_NO_RESPONSE); + return; + } final FillWindow fillWindow = response.getFillWindow(); if (fillWindow != null) { diff --git a/core/java/android/service/autofill/augmented/Helper.java b/core/java/android/service/autofill/augmented/Helper.java new file mode 100644 index 000000000000..501696f99c66 --- /dev/null +++ b/core/java/android/service/autofill/augmented/Helper.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.service.autofill.augmented; + +import android.annotation.NonNull; +import android.content.ComponentName; +import android.metrics.LogMaker; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +/** @hide */ +public final class Helper { + + private static final MetricsLogger sMetricsLogger = new MetricsLogger(); + + /** + * Logs a {@code MetricsEvent.AUTOFILL_AUGMENTED_RESPONSE} event. + */ + public static void logResponse(int type, @NonNull String servicePackageName, + @NonNull ComponentName componentName, int mSessionId, long durationMs) { + final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_AUGMENTED_RESPONSE) + .setType(type) + .setComponentName(componentName) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SESSION_ID, mSessionId) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_DURATION, durationMs); + System.out.println("LOGGGO: " + log.getEntries()); // felipeal: tmp + sMetricsLogger.write(log); + } + + private Helper() { + throw new UnsupportedOperationException("contains only static methods"); + } +} diff --git a/core/java/android/util/LauncherIcons.java b/core/java/android/util/LauncherIcons.java index cc9991a9be20..8501eb5883d5 100644 --- a/core/java/android/util/LauncherIcons.java +++ b/core/java/android/util/LauncherIcons.java @@ -15,6 +15,7 @@ */ package android.util; +import android.app.ActivityThread; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; @@ -102,16 +103,17 @@ public final class LauncherIcons { } public Drawable getBadgedDrawable(Drawable base, int foregroundRes, int backgroundColor) { - Resources sysRes = Resources.getSystem(); + Resources overlayableRes = + ActivityThread.currentActivityThread().getApplication().getResources(); - Drawable badgeShadow = sysRes.getDrawable( + Drawable badgeShadow = overlayableRes.getDrawable( com.android.internal.R.drawable.ic_corp_icon_badge_shadow); - Drawable badgeColor = sysRes.getDrawable( + Drawable badgeColor = overlayableRes.getDrawable( com.android.internal.R.drawable.ic_corp_icon_badge_color) .getConstantState().newDrawable().mutate(); - Drawable badgeForeground = sysRes.getDrawable(foregroundRes); + Drawable badgeForeground = overlayableRes.getDrawable(foregroundRes); badgeForeground.setTint(backgroundColor); Drawable[] drawables = base == null diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 5872d3f7f785..35ea8964a342 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -48,6 +48,7 @@ import android.service.autofill.FillEventHistory; import android.service.autofill.UserData; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.DebugUtils; import android.util.Log; import android.util.Slog; import android.util.SparseArray; @@ -229,7 +230,8 @@ public final class AutofillManager { /** @hide */ public static final int FLAG_ADD_CLIENT_VERBOSE = 0x4; /** @hide */ public static final int FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY = 0x8; - /** @hide */ public static final int FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY = 0x1; + // NOTE: flag below is used by the session start receiver only, hence it can have values above + /** @hide */ public static final int RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY = 0x1; /** @hide */ public static final int DEFAULT_LOGGING_LEVEL = Build.IS_DEBUGGABLE @@ -521,7 +523,7 @@ public final class AutofillManager { private boolean mForAugmentedAutofillOnly; /** - * When set, standard autofill is enabled, but sessions can still be created for augmented + * When set, standard autofill is disabled, but sessions can still be created for augmented * autofill only. */ @GuardedBy("mLock") @@ -969,6 +971,13 @@ public final class AutofillManager { startSessionLocked(id, null, value, flags); } else { // Update focus on existing session. + if (mForAugmentedAutofillOnly && (flags & FLAG_MANUAL_REQUEST) != 0) { + if (sDebug) { + Log.d(TAG, "notifyViewEntered(" + id + "): resetting " + + "mForAugmentedAutofillOnly on manual request"); + } + mForAugmentedAutofillOnly = false; + } updateSessionLocked(id, null, value, ACTION_VIEW_ENTERED, flags); } addEnteredIdLocked(id); @@ -1126,6 +1135,13 @@ public final class AutofillManager { startSessionLocked(id, bounds, null, flags); } else { // Update focus on existing session. + if (mForAugmentedAutofillOnly && (flags & FLAG_MANUAL_REQUEST) != 0) { + if (sDebug) { + Log.d(TAG, "notifyViewEntered(" + id + "): resetting " + + "mForAugmentedAutofillOnly on manual request"); + } + mForAugmentedAutofillOnly = false; + } updateSessionLocked(id, bounds, null, ACTION_VIEW_ENTERED, flags); } addEnteredIdLocked(id); @@ -1695,6 +1711,16 @@ public final class AutofillManager { + ", enabledAugmentedOnly=" + mEnabledForAugmentedAutofillOnly + ", enteredIds=" + mEnteredIds); } + // We need to reset the augmented-only state when a manual request is made, as it's possible + // that the service returned null for the first request and now the user is manually + // requesting autofill to trigger a custom UI provided by the service. + if (mForAugmentedAutofillOnly && !mEnabledForAugmentedAutofillOnly + && (flags & FLAG_MANUAL_REQUEST) != 0) { + if (sVerbose) { + Log.v(TAG, "resetting mForAugmentedAutofillOnly on manual autofill request"); + } + mForAugmentedAutofillOnly = false; + } if (mState != STATE_UNKNOWN && !isFinishedLocked() && (flags & FLAG_MANUAL_REQUEST) == 0) { if (sVerbose) { Log.v(TAG, "not automatically starting session for " + id @@ -1717,7 +1743,7 @@ public final class AutofillManager { mState = STATE_ACTIVE; } final int extraFlags = receiver.getOptionalExtraIntResult(0); - if ((extraFlags & FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) != 0) { + if ((extraFlags & RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) != 0) { if (sDebug) Log.d(TAG, "startSession(" + componentName + "): for augmented only"); mForAugmentedAutofillOnly = true; } @@ -2011,10 +2037,20 @@ public final class AutofillManager { public static final int SET_STATE_FLAG_DEBUG = 0x08; /** @hide */ public static final int SET_STATE_FLAG_VERBOSE = 0x10; + /** @hide */ + public static final int SET_STATE_FLAG_FOR_AUTOFILL_ONLY = 0x20; private void setState(int flags) { - if (sVerbose) Log.v(TAG, "setState(" + flags + ")"); + if (sVerbose) { + Log.v(TAG, "setState(" + flags + ": " + DebugUtils.flagsToString(AutofillManager.class, + "SET_STATE_FLAG_", flags) + ")"); + } synchronized (mLock) { + if ((flags & SET_STATE_FLAG_FOR_AUTOFILL_ONLY) != 0) { + mForAugmentedAutofillOnly = true; + // NOTE: returning right away as this is the only flag set, at least currently... + return; + } mEnabled = (flags & SET_STATE_FLAG_ENABLED) != 0; if (!mEnabled || (flags & SET_STATE_FLAG_RESET_SESSION) != 0) { // Reset the session state @@ -2390,7 +2426,7 @@ public final class AutofillManager { } } - if (sessionFinishedState != 0) { + if (sessionFinishedState != STATE_UNKNOWN) { // Callback call was "hijacked" to also update the session state. setSessionFinished(sessionFinishedState, /* autofillableIds= */ null); } diff --git a/core/java/android/webkit/WebViewLibraryLoader.java b/core/java/android/webkit/WebViewLibraryLoader.java index 5a6aebaaad51..be49fc434c79 100644 --- a/core/java/android/webkit/WebViewLibraryLoader.java +++ b/core/java/android/webkit/WebViewLibraryLoader.java @@ -181,9 +181,9 @@ public class WebViewLibraryLoader { boolean is64Bit = VMRuntime.getRuntime().is64Bit(); // On 64-bit address space is really cheap and we can reserve 1GB which is plenty. // On 32-bit it's fairly scarce and we should keep it to a realistic number that - // permits some future growth but doesn't hog space: we use 100MB which is more than 2x - // the current requirement. - long addressSpaceToReserve = is64Bit ? 1 * 1024 * 1024 * 1024 : 100 * 1024 * 1024; + // permits some future growth but doesn't hog space: we use 130MB which is roughly + // what was calculated on older OS versions in practice. + long addressSpaceToReserve = is64Bit ? 1 * 1024 * 1024 * 1024 : 130 * 1024 * 1024; sAddressSpaceReserved = nativeReserveAddressSpace(addressSpaceToReserve); if (sAddressSpaceReserved) { diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index b7d838edadc5..740753d9e5f1 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -779,7 +779,7 @@ public class ChooserActivity extends ResolverActivity { } } } catch (SecurityException | NullPointerException e) { - Log.w(TAG, "Error loading file preview", e); + logContentPreviewWarning(uri); } if (TextUtils.isEmpty(fileName)) { @@ -793,6 +793,14 @@ public class ChooserActivity extends ResolverActivity { return new FileInfo(fileName, hasThumbnail); } + private void logContentPreviewWarning(Uri uri) { + // The ContentResolver already logs the exception. Log something more informative. + Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If " + + "desired, consider using Intent#createChooser to launch the ChooserActivity, " + + "and set your Intent's clipData and flags in accordance with that method's " + + "documentation"); + } + private ViewGroup displayFileContentPreview(Intent targetIntent, LayoutInflater layoutInflater, ViewGroup convertView, ViewGroup parent) { @@ -1664,7 +1672,7 @@ public class ChooserActivity extends ResolverActivity { try { return ImageUtils.loadThumbnail(getContentResolver(), uri, size); } catch (IOException | NullPointerException | SecurityException ex) { - Log.w(TAG, "Error loading preview thumbnail for uri: " + uri.toString(), ex); + logContentPreviewWarning(uri); } return null; } diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 31f7d6f51ff8..9a10210739e1 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -630,7 +630,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind drawingBounds.bottom -= framePadding.bottom - frameOffsets.bottom; } - Drawable bg = getBackground(); + // Need to call super here as we pretend to be having the original background. + Drawable bg = super.getBackground(); if (bg != null) { bg.setBounds(drawingBounds); } @@ -975,6 +976,18 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } } + @Override + public void setBackgroundDrawable(Drawable background) { + + // TODO: This should route through setWindowBackground, but late in the release to make this + // change. + if (mOriginalBackgroundDrawable != background) { + mOriginalBackgroundDrawable = background; + updateBackgroundDrawable(); + drawableChanged(); + } + } + public void setWindowFrame(Drawable drawable) { if (getForeground() != drawable) { setForeground(drawable); @@ -1161,12 +1174,16 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind // If we didn't request fullscreen layout, but we still got it because of the // mForceWindowDrawsBarBackgrounds flag, also consume top inset. + // If we should always consume system bars, only consume that if the app wanted to go to + // fullscreen, as othrewise we can expect the app to handle it. + boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0 + || (attrs.flags & FLAG_FULLSCREEN) != 0; boolean consumingStatusBar = (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0 && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0 && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0 && mForceWindowDrawsBarBackgrounds && mLastTopInset != 0 - || mLastShouldAlwaysConsumeSystemBars; + || (mLastShouldAlwaysConsumeSystemBars && fullscreen); int consumedTop = consumingStatusBar ? mLastTopInset : 0; int consumedRight = consumingNavBar ? mLastRightInset : 0; @@ -1213,14 +1230,22 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind * are set. */ private void updateBackgroundDrawable() { + // Background insets can be null if super constructor calls setBackgroundDrawable. + if (mBackgroundInsets == null) { + mBackgroundInsets = Insets.NONE; + } if (mBackgroundInsets.equals(mLastBackgroundInsets) && mLastOriginalBackgroundDrawable == mOriginalBackgroundDrawable) { return; } if (mOriginalBackgroundDrawable == null || mBackgroundInsets.equals(Insets.NONE)) { - setBackground(mOriginalBackgroundDrawable); + + // Call super since we are intercepting setBackground on this class. + super.setBackgroundDrawable(mOriginalBackgroundDrawable); } else { - setBackground(new InsetDrawable(mOriginalBackgroundDrawable, + + // Call super since we are intercepting setBackground on this class. + super.setBackgroundDrawable(new InsetDrawable(mOriginalBackgroundDrawable, mBackgroundInsets.left, mBackgroundInsets.top, mBackgroundInsets.right, mBackgroundInsets.bottom) { @@ -1238,6 +1263,11 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind mLastOriginalBackgroundDrawable = mOriginalBackgroundDrawable; } + @Override + public Drawable getBackground() { + return mOriginalBackgroundDrawable; + } + private int calculateStatusBarColor() { return calculateBarColor(mWindow.getAttributes().flags, FLAG_TRANSLUCENT_STATUS, mSemiTransparentBarColor, mWindow.mStatusBarColor, @@ -1527,10 +1557,14 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind return; } - setPadding(mFramePadding.left + mBackgroundPadding.left, - mFramePadding.top + mBackgroundPadding.top, - mFramePadding.right + mBackgroundPadding.right, - mFramePadding.bottom + mBackgroundPadding.bottom); + // Fields can be null if super constructor calls setBackgroundDrawable. + Rect framePadding = mFramePadding != null ? mFramePadding : new Rect(); + Rect backgroundPadding = mBackgroundPadding != null ? mBackgroundPadding : new Rect(); + + setPadding(framePadding.left + backgroundPadding.left, + framePadding.top + backgroundPadding.top, + framePadding.right + backgroundPadding.right, + framePadding.bottom + backgroundPadding.bottom); requestLayout(); invalidate(); @@ -1550,8 +1584,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind if (bg != null) { if (fg == null) { opacity = bg.getOpacity(); - } else if (mFramePadding.left <= 0 && mFramePadding.top <= 0 - && mFramePadding.right <= 0 && mFramePadding.bottom <= 0) { + } else if (framePadding.left <= 0 && framePadding.top <= 0 + && framePadding.right <= 0 && framePadding.bottom <= 0) { // If the frame padding is zero, then we can be opaque // if either the frame -or- the background is opaque. int fop = fg.getOpacity(); diff --git a/core/proto/android/app/settings_enums.proto b/core/proto/android/app/settings_enums.proto index 15944023fb14..1160b357a1fb 100644 --- a/core/proto/android/app/settings_enums.proto +++ b/core/proto/android/app/settings_enums.proto @@ -2364,4 +2364,9 @@ enum PageId { // Settings > Apps and notifications > Notifications > Gentle notifications GENTLE_NOTIFICATIONS_SCREEN = 1715; + + // OPEN: Settings > System > Gestures > Global Actions Panel + // CATEGORY: SETTINGS + // OS: Q + GLOBAL_ACTIONS_PANEL_SETTINGS = 1800; } diff --git a/core/res/res/drawable/ic_corp_icon_badge_color.xml b/core/res/res/drawable/ic_corp_icon_badge_color.xml index 6dba2779bc53..bb2d11753303 100644 --- a/core/res/res/drawable/ic_corp_icon_badge_color.xml +++ b/core/res/res/drawable/ic_corp_icon_badge_color.xml @@ -19,12 +19,15 @@ Copyright (C) 2016 The Android Open Source Project android:height="64dp" android:viewportWidth="64" android:viewportHeight="64"> - - <path - android:fillColor="#fcfcfc" - android:strokeColor="#e8eaed" - android:strokeWidth="0.25" - android:pathData="M62,50A12,12,0,1,1,50,38,12,12,0,0,1,62,50" /> - <path - android:pathData="M 0 0 H 64 V 64 H 0 V 0 Z" /> + <group + android:scaleX=".24" + android:scaleY=".24" + android:translateX="38" + android:translateY="38"> + <path + android:fillColor="#fcfcfc" + android:strokeColor="#e8eaed" + android:strokeWidth="0.25" + android:pathData="@string/config_icon_mask" /> + </group> </vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_corp_icon_badge_shadow.xml b/core/res/res/drawable/ic_corp_icon_badge_shadow.xml index f33ed1f010be..cb29f6c39be4 100644 --- a/core/res/res/drawable/ic_corp_icon_badge_shadow.xml +++ b/core/res/res/drawable/ic_corp_icon_badge_shadow.xml @@ -19,31 +19,52 @@ Copyright (C) 2016 The Android Open Source Project android:height="64dp" android:viewportWidth="64" android:viewportHeight="64"> - - <path - android:fillColor="#000000" - android:fillAlpha="0.06" - android:strokeAlpha="0.06" - android:strokeWidth="1" - android:pathData="M62,51.25a12,12,0,1,1-12-12,12,12,0,0,1,12,12" /> - <path - android:pathData="M 0 0 H 64 V 64 H 0 V 0 Z" /> - <path - android:fillColor="#000000" - android:fillAlpha="0.06" - android:strokeAlpha="0.06" - android:strokeWidth="1" - android:pathData="M62,52.28A12,12,0,1,1,50.53,39.76,12,12,0,0,1,62,52.28" /> - <path - android:fillColor="#000000" - android:fillAlpha="0.06" - android:strokeAlpha="0.06" - android:strokeWidth="1" - android:pathData="M62,50.75a12,12,0,1,1-12-12,12,12,0,0,1,12,12" /> - <path - android:fillColor="#000000" - android:fillAlpha="0.06" - android:strokeAlpha="0.06" - android:strokeWidth="1" - android:pathData="M62,50.25a12,12,0,1,1-12-12,12,12,0,0,1,12,12" /> + <group + android:scaleX=".24" + android:scaleY=".24" + android:translateX="38" + android:translateY="39.25"> + <path + android:fillColor="#000000" + android:fillAlpha="0.06" + android:strokeAlpha="0.06" + android:strokeWidth="1" + android:pathData="@string/config_icon_mask" /> + </group> + <group + android:scaleX=".24" + android:scaleY=".24" + android:translateX="38" + android:translateY="39.75"> + <path + android:fillColor="#000000" + android:fillAlpha="0.06" + android:strokeAlpha="0.06" + android:strokeWidth="1" + android:pathData="@string/config_icon_mask" /> + </group> + <group + android:scaleX=".24" + android:scaleY=".24" + android:translateX="38" + android:translateY="38.75"> + <path + android:fillColor="#000000" + android:fillAlpha="0.06" + android:strokeAlpha="0.06" + android:strokeWidth="1" + android:pathData="@string/config_icon_mask" /> + </group> + <group + android:scaleX=".24" + android:scaleY=".24" + android:translateX="38" + android:translateY="38.25"> + <path + android:fillColor="#000000" + android:fillAlpha="0.06" + android:strokeAlpha="0.06" + android:strokeWidth="1" + android:pathData="@string/config_icon_mask" /> + </group> </vector>
\ No newline at end of file diff --git a/core/res/res/layout-car/car_preference.xml b/core/res/res/layout-car/car_preference.xml index ae3d63bd2d6a..b138f4d7cf50 100644 --- a/core/res/res/layout-car/car_preference.xml +++ b/core/res/res/layout-car/car_preference.xml @@ -27,20 +27,20 @@ <com.android.internal.widget.PreferenceImageView android:id="@id/icon" - android:layout_width="@*android:dimen/car_primary_icon_size" - android:layout_height="@*android:dimen/car_primary_icon_size" + android:layout_width="@dimen/car_preference_icon_size" + android:layout_height="@dimen/car_preference_icon_size" android:layout_alignParentStart="true" android:layout_centerVertical="true" - android:layout_marginBottom="@*android:dimen/car_padding_2" + android:layout_marginBottom="@dimen/car_preference_row_vertical_margin" android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd" - android:layout_marginTop="@android:dimen/car_padding_2"/> + android:layout_marginTop="@dimen/car_preference_row_vertical_margin"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" - android:layout_marginBottom="@*android:dimen/car_padding_2" - android:layout_marginTop="@*android:dimen/car_padding_2" + android:layout_marginBottom="@dimen/car_preference_row_vertical_margin" + android:layout_marginTop="@dimen/car_preference_row_vertical_margin" android:layout_toEndOf="@id/icon" android:layout_toStartOf="@id/widget_frame" android:orientation="vertical"> diff --git a/core/res/res/layout-car/car_preference_category.xml b/core/res/res/layout-car/car_preference_category.xml index d1f73421e185..b674487cffa7 100644 --- a/core/res/res/layout-car/car_preference_category.xml +++ b/core/res/res/layout-car/car_preference_category.xml @@ -22,25 +22,25 @@ android:background="?android:attr/selectableItemBackground" android:focusable="true" android:gravity="center_vertical" - android:minHeight="@*android:dimen/car_card_header_height" + android:minHeight="@dimen/car_card_header_height" android:orientation="horizontal" android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" android:paddingStart="?android:attr/listPreferredItemPaddingStart"> <com.android.internal.widget.PreferenceImageView android:id="@id/icon" - android:layout_width="@*android:dimen/car_primary_icon_size" - android:layout_height="@*android:dimen/car_primary_icon_size" + android:layout_width="@dimen/car_preference_category_icon_size" + android:layout_height="@dimen/car_preference_category_icon_size" android:layout_gravity="center_vertical" - android:layout_marginBottom="@dimen/car_padding_2" + android:layout_marginBottom="@dimen/car_preference_row_vertical_margin" android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd" - android:layout_marginTop="@*android:dimen/car_padding_2"/> + android:layout_marginTop="@dimen/car_preference_row_vertical_margin"/> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="@*android:dimen/car_padding_2" - android:layout_marginTop="@*android:dimen/car_padding_2" + android:layout_marginBottom="@dimen/car_preference_row_vertical_margin" + android:layout_marginTop="@dimen/car_preference_row_vertical_margin" android:orientation="vertical"> <TextView diff --git a/core/res/res/values/dimens_car.xml b/core/res/res/values/dimens_car.xml index f22a91ff75c1..880f9ccebd6e 100644 --- a/core/res/res/values/dimens_car.xml +++ b/core/res/res/values/dimens_car.xml @@ -137,4 +137,8 @@ <!-- Dialog image margin start --> <dimen name="image_margin_start">@*android:dimen/car_keyline_1</dimen> + <dimen name="car_preference_icon_size">@dimen/car_primary_icon_size</dimen> + <dimen name="car_preference_category_icon_size">@dimen/car_primary_icon_size</dimen> + <dimen name="car_preference_row_vertical_margin">@dimen/car_padding_2</dimen> + </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index dd0a6e605f60..b07e7ef0bd05 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1549,11 +1549,11 @@ <!-- Message shown during acqusition when the user's face is tilted too high or too low [CHAR LIMIT=50] --> <string name="face_acquired_tilt_too_extreme">Turn your head a little less.</string> <!-- Message shown during acquisiton when the user's face is tilted too far left or right [CHAR LIMIT=50] --> - <string name="face_acquired_roll_too_extreme">Please straighten your head vertically.</string> + <string name="face_acquired_roll_too_extreme">Turn your head a little less.</string> <!-- Message shown during acquisition when the user's face is obscured [CHAR LIMIT=50] --> - <string name="face_acquired_obscured">Clear the space between your head and the phone.</string> + <string name="face_acquired_obscured">Remove anything hiding your face.</string> <!-- Message shown during acquisition when the sensor is dirty [CHAR LIMIT=50] --> - <string name="face_acquired_sensor_dirty">Please clean the camera.</string> + <string name="face_acquired_sensor_dirty">Clean the sensor at the top edge of the screen.</string> <!-- Array containing custom messages shown during face acquisition from vendor. Vendor is expected to add and translate these strings --> <string-array name="face_acquired_vendor"> </string-array> diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java index e76754582fe9..9fa04ef2dda2 100644 --- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java +++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java @@ -636,6 +636,7 @@ public class SettingsBackupTest { Settings.Secure.ENABLED_NOTIFICATION_LISTENERS, Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES, Settings.Secure.ENABLED_PRINT_SERVICES, + Settings.Secure.GLOBAL_ACTIONS_PANEL_AVAILABLE, Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, Settings.Secure.INCALL_BACK_BUTTON_BEHAVIOR, Settings.Secure.INPUT_METHOD_SELECTOR_VISIBILITY, @@ -720,7 +721,8 @@ public class SettingsBackupTest { Settings.Secure.LOCATION_ACCESS_CHECK_DELAY_MILLIS, Settings.Secure.BIOMETRIC_DEBUG_ENABLED, Settings.Secure.FACE_UNLOCK_ATTENTION_REQUIRED, - Settings.Secure.FACE_UNLOCK_DIVERSITY_REQUIRED); + Settings.Secure.FACE_UNLOCK_DIVERSITY_REQUIRED, + Settings.Secure.FACE_UNLOCK_EDUCATION_INFO_DISPLAYED); @Test public void systemSettingsBackedUpOrBlacklisted() { @@ -798,4 +800,3 @@ public class SettingsBackupTest { } } - diff --git a/libs/protoutil/src/ProtoFileReader.cpp b/libs/protoutil/src/ProtoFileReader.cpp index c7f1129fbbaa..bbb1fe374f0e 100644 --- a/libs/protoutil/src/ProtoFileReader.cpp +++ b/libs/protoutil/src/ProtoFileReader.cpp @@ -99,7 +99,6 @@ ProtoFileReader::next() // Shouldn't get to here. Always call hasNext() before calling next(). return 0; } - mPos++; return mBuffer[mOffset++]; } @@ -131,7 +130,6 @@ ProtoFileReader::move(size_t amt) const size_t chunk = mMaxOffset - mOffset > amt ? amt : mMaxOffset - mOffset; mOffset += chunk; - mPos += chunk; amt -= chunk; } } diff --git a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java index b4be21987cf7..04e7bab0542b 100644 --- a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java +++ b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java @@ -357,8 +357,10 @@ public class GpsNetInitiatedHandler { } } - // Sets the NI notification. - private synchronized void setNiNotification(GpsNiNotification notif) { + /** + * Posts a notification in the status bar using the contents in {@code notif} object. + */ + public synchronized void setNiNotification(GpsNiNotification notif) { NotificationManager notificationManager = (NotificationManager) mContext .getSystemService(Context.NOTIFICATION_SERVICE); if (notificationManager == null) { @@ -539,14 +541,14 @@ public class GpsNetInitiatedHandler { */ static private String decodeString(String original, boolean isHex, int coding) { + if (coding == GPS_ENC_NONE) { + return original; + } + String decoded = original; byte[] input = stringToByteArray(original, isHex); switch (coding) { - case GPS_ENC_NONE: - decoded = original; - break; - case GPS_ENC_SUPL_GSM_DEFAULT: decoded = decodeGSMPackedString(input); break; diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index eeb7655abff9..ce9b07dd0c0e 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -838,6 +838,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, } if (mAudioCapturePolicy != null) { AudioManager.unregisterAudioPolicyAsyncStatic(mAudioCapturePolicy); + mAudioCapturePolicy = null; } native_release(); mState = STATE_UNINITIALIZED; diff --git a/media/java/android/media/AudioRecordingConfiguration.java b/media/java/android/media/AudioRecordingConfiguration.java index 74e661834818..874a215e4975 100644 --- a/media/java/android/media/AudioRecordingConfiguration.java +++ b/media/java/android/media/AudioRecordingConfiguration.java @@ -157,7 +157,7 @@ public final class AudioRecordingConfiguration implements Parcelable { return new AudioRecordingConfiguration( /*anonymized uid*/ -1, in.mClientSessionId, in.mClientSource, in.mClientFormat, in.mDeviceFormat, in.mPatchHandle, "" /*empty package name*/, - /*anonymized portId*/ -1, in.mClientSilenced, in.mDeviceSource, in.mClientEffects, + in.mClientPortId, in.mClientSilenced, in.mDeviceSource, in.mClientEffects, in.mDeviceEffects); } diff --git a/packages/CaptivePortalLogin/AndroidManifest.xml b/packages/CaptivePortalLogin/AndroidManifest.xml index a8f9c3b6aa87..86d6d442c672 100644 --- a/packages/CaptivePortalLogin/AndroidManifest.xml +++ b/packages/CaptivePortalLogin/AndroidManifest.xml @@ -31,6 +31,7 @@ <uses-permission android:name="android.permission.MAINLINE_NETWORK_STACK" /> <application android:label="@string/app_name" + android:icon="@drawable/app_icon" android:usesCleartextTraffic="true" android:supportsRtl="true" > <activity diff --git a/packages/CaptivePortalLogin/res/drawable/app_icon.xml b/packages/CaptivePortalLogin/res/drawable/app_icon.xml new file mode 100644 index 000000000000..456ca83f5227 --- /dev/null +++ b/packages/CaptivePortalLogin/res/drawable/app_icon.xml @@ -0,0 +1,26 @@ +<!-- + 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. +--> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background> + <color android:color="@*android:color/accent_device_default_light" /> + </background> + <foreground> + <inset + android:drawable="@drawable/maybe_wifi" + android:inset="25%"> + </inset> + </foreground> +</adaptive-icon> diff --git a/packages/CaptivePortalLogin/res/drawable/maybe_wifi.xml b/packages/CaptivePortalLogin/res/drawable/maybe_wifi.xml new file mode 100644 index 000000000000..207aade406ef --- /dev/null +++ b/packages/CaptivePortalLogin/res/drawable/maybe_wifi.xml @@ -0,0 +1,27 @@ +<!-- +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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="26.0dp" + android:height="24.0dp" + android:viewportWidth="26.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#4DFFFFFF" + android:pathData="M19.1,14l-3.4,0l0,-1.5c0,-1.8 0.8,-2.8 1.5,-3.4C18.1,8.3 19.200001,8 20.6,8c1.2,0 2.3,0.3 3.1,0.8l1.9,-2.3C25.1,6.1 20.299999,2.1 13,2.1S0.9,6.1 0.4,6.5L13,22l0,0l0,0l0,0l0,0l6.5,-8.1L19.1,14z"/> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M19.5,17.799999c0,-0.8 0.1,-1.3 0.2,-1.6c0.2,-0.3 0.5,-0.7 1.1,-1.2c0.4,-0.4 0.7,-0.8 1,-1.1s0.4,-0.8 0.4,-1.2c0,-0.5 -0.1,-0.9 -0.4,-1.2c-0.3,-0.3 -0.7,-0.4 -1.2,-0.4c-0.4,0 -0.8,0.1 -1.1,0.3c-0.3,0.2 -0.4,0.6 -0.4,1.1l-1.9,0c0,-1 0.3,-1.7 1,-2.2c0.6,-0.5 1.5,-0.8 2.5,-0.8c1.1,0 2,0.3 2.6,0.8c0.6,0.5 0.9,1.3 0.9,2.3c0,0.7 -0.2,1.3 -0.6,1.8c-0.4,0.6 -0.9,1.1 -1.5,1.6c-0.3,0.3 -0.5,0.5 -0.6,0.7c-0.1,0.2 -0.1,0.6 -0.1,1L19.5,17.700001zM21.4,21l-1.9,0l0,-1.8l1.9,0L21.4,21z"/> +</vector> diff --git a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java index bacec78e5699..d6355bc111c2 100644 --- a/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java +++ b/packages/NetworkStack/src/com/android/server/connectivity/NetworkMonitor.java @@ -279,8 +279,8 @@ public class NetworkMonitor extends StateMachine { private final Context mContext; private final INetworkMonitorCallbacks mCallback; + private final Network mCleartextDnsNetwork; private final Network mNetwork; - private final Network mNonPrivateDnsBypassNetwork; private final TelephonyManager mTelephonyManager; private final WifiManager mWifiManager; private final ConnectivityManager mCm; @@ -370,8 +370,8 @@ public class NetworkMonitor extends StateMachine { mCallback = cb; mDependencies = deps; mDetectionStatsUtils = detectionStatsUtils; - mNonPrivateDnsBypassNetwork = network; - mNetwork = deps.getPrivateDnsBypassNetwork(network); + mNetwork = network; + mCleartextDnsNetwork = deps.getPrivateDnsBypassNetwork(network); mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -496,7 +496,7 @@ public class NetworkMonitor extends StateMachine { @Override protected void log(String s) { - if (DBG) Log.d(TAG + "/" + mNetwork.toString(), s); + if (DBG) Log.d(TAG + "/" + mCleartextDnsNetwork.toString(), s); } private void validationLog(int probeType, Object url, String msg) { @@ -769,7 +769,7 @@ public class NetworkMonitor extends StateMachine { case CMD_LAUNCH_CAPTIVE_PORTAL_APP: final Bundle appExtras = new Bundle(); // OneAddressPerFamilyNetwork is not parcelable across processes. - final Network network = new Network(mNetwork); + final Network network = new Network(mCleartextDnsNetwork); appExtras.putParcelable(ConnectivityManager.EXTRA_NETWORK, network); final CaptivePortalProbeResult probeRes = mLastPortalProbeResult; appExtras.putString(EXTRA_CAPTIVE_PORTAL_URL, probeRes.detectUrl); @@ -881,7 +881,7 @@ public class NetworkMonitor extends StateMachine { CustomIntentReceiver(String action, int token, int what) { mToken = token; mWhat = what; - mAction = action + "_" + mNetwork.getNetworkHandle() + "_" + token; + mAction = action + "_" + mCleartextDnsNetwork.getNetworkHandle() + "_" + token; mContext.registerReceiver(this, new IntentFilter(mAction)); } public PendingIntent getPendingIntent() { @@ -994,7 +994,8 @@ public class NetworkMonitor extends StateMachine { private void resolveStrictModeHostname() { try { // Do a blocking DNS resolution using the network-assigned nameservers. - final InetAddress[] ips = mNetwork.getAllByName(mPrivateDnsProviderHostname); + final InetAddress[] ips = mCleartextDnsNetwork.getAllByName( + mPrivateDnsProviderHostname); mPrivateDnsConfig = new PrivateDnsConfig(mPrivateDnsProviderHostname, ips); validationLog("Strict mode hostname resolved: " + mPrivateDnsConfig); } catch (UnknownHostException uhe) { @@ -1033,7 +1034,7 @@ public class NetworkMonitor extends StateMachine { + oneTimeHostnameSuffix; final Stopwatch watch = new Stopwatch().start(); try { - final InetAddress[] ips = mNonPrivateDnsBypassNetwork.getAllByName(host); + final InetAddress[] ips = mNetwork.getAllByName(host); final long time = watch.stop(); final String strIps = Arrays.toString(ips); final boolean success = (ips != null && ips.length > 0); @@ -1506,7 +1507,7 @@ public class NetworkMonitor extends StateMachine { final int oldTag = TrafficStats.getAndSetThreadStatsTag( TrafficStatsConstants.TAG_SYSTEM_PROBE); - mDependencies.getDnsResolver().query(mNetwork, host, DnsResolver.FLAG_EMPTY, + mDependencies.getDnsResolver().query(mCleartextDnsNetwork, host, DnsResolver.FLAG_EMPTY, r -> r.run() /* executor */, null /* cancellationSignal */, callback); TrafficStats.setThreadStatsTag(oldTag); @@ -1565,7 +1566,7 @@ public class NetworkMonitor extends StateMachine { final int oldTag = TrafficStats.getAndSetThreadStatsTag( TrafficStatsConstants.TAG_SYSTEM_PROBE); try { - urlConnection = (HttpURLConnection) mNetwork.openConnection(url); + urlConnection = (HttpURLConnection) mCleartextDnsNetwork.openConnection(url); urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC); urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); @@ -1814,7 +1815,7 @@ public class NetworkMonitor extends StateMachine { private void logNetworkEvent(int evtype) { int[] transports = mNetworkCapabilities.getTransportTypes(); - mMetricsLog.log(mNetwork, transports, new NetworkEvent(evtype)); + mMetricsLog.log(mCleartextDnsNetwork, transports, new NetworkEvent(evtype)); } private int networkEventType(ValidationStage s, EvaluationResult r) { @@ -1836,7 +1837,7 @@ public class NetworkMonitor extends StateMachine { private void maybeLogEvaluationResult(int evtype) { if (mEvaluationTimer.isRunning()) { int[] transports = mNetworkCapabilities.getTransportTypes(); - mMetricsLog.log(mNetwork, transports, + mMetricsLog.log(mCleartextDnsNetwork, transports, new NetworkEvent(evtype, mEvaluationTimer.stop())); mEvaluationTimer.reset(); } @@ -1850,7 +1851,7 @@ public class NetworkMonitor extends StateMachine { .setReturnCode(probeResult) .setDurationMs(durationMs) .build(); - mMetricsLog.log(mNetwork, transports, ev); + mMetricsLog.log(mCleartextDnsNetwork, transports, ev); } @VisibleForTesting diff --git a/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java index 0dc1cbf8a984..6f5c27edb9d5 100644 --- a/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java +++ b/packages/NetworkStack/tests/src/com/android/server/connectivity/NetworkMonitorTest.java @@ -42,10 +42,10 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -66,6 +66,7 @@ import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.captiveportal.CaptivePortalProbeResult; import android.net.metrics.IpConnectivityLog; +import android.net.shared.PrivateDnsConfig; import android.net.util.SharedLog; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; @@ -73,6 +74,7 @@ import android.os.Bundle; import android.os.ConditionVariable; import android.os.Handler; import android.os.Looper; +import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; import android.provider.Settings; @@ -131,7 +133,8 @@ public class NetworkMonitorTest { private @Mock Random mRandom; private @Mock NetworkMonitor.Dependencies mDependencies; private @Mock INetworkMonitorCallbacks mCallbacks; - private @Spy Network mNetwork = new Network(TEST_NETID); + private @Spy Network mCleartextDnsNetwork = new Network(TEST_NETID); + private @Mock Network mNetwork; private @Mock DataStallStatsUtils mDataStallStatsUtils; private @Mock WifiInfo mWifiInfo; private @Captor ArgumentCaptor<String> mNetworkTestedRedirectUrlCaptor; @@ -166,35 +169,97 @@ public class NetworkMonitorTest { private static final NetworkCapabilities NO_INTERNET_CAPABILITIES = new NetworkCapabilities() .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); - private void setDnsAnswers(String[] answers) throws UnknownHostException { - if (answers == null) { - doThrow(new UnknownHostException()).when(mNetwork).getAllByName(any()); - doNothing().when(mDnsResolver).query(any(), any(), anyInt(), any(), any(), any()); - return; + /** + * Fakes DNS responses. + * + * Allows test methods to configure the IP addresses that will be resolved by + * Network#getAllByName and by DnsResolver#query. + */ + class FakeDns { + private final ArrayMap<String, List<InetAddress>> mAnswers = new ArrayMap<>(); + private boolean mNonBypassPrivateDnsWorking = true; + + /** Whether DNS queries on mNonBypassPrivateDnsWorking should succeed. */ + private void setNonBypassPrivateDnsWorking(boolean working) { + mNonBypassPrivateDnsWorking = working; } - List<InetAddress> answerList = new ArrayList<>(); - for (String answer : answers) { - answerList.add(InetAddresses.parseNumericAddress(answer)); + /** Clears all DNS entries. */ + private synchronized void clearAll() { + mAnswers.clear(); } - InetAddress[] answerArray = answerList.toArray(new InetAddress[0]); - doReturn(answerArray).when(mNetwork).getAllByName(any()); + /** Returns the answer for a given name on the given mock network. */ + private synchronized List<InetAddress> getAnswer(Object mock, String hostname) { + if (mock == mNetwork && !mNonBypassPrivateDnsWorking) { + return null; + } + if (mAnswers.containsKey(hostname)) { + return mAnswers.get(hostname); + } + return mAnswers.get("*"); + } - doAnswer((invocation) -> { - Executor executor = (Executor) invocation.getArgument(3); - DnsResolver.Callback<List<InetAddress>> callback = invocation.getArgument(5); - new Handler(Looper.getMainLooper()).post(() -> { - executor.execute(() -> callback.onAnswer(answerList, 0)); - }); - return null; - }).when(mDnsResolver).query(eq(mNetwork), any(), anyInt(), any(), any(), any()); + /** Sets the answer for a given name. */ + private synchronized void setAnswer(String hostname, String[] answer) + throws UnknownHostException { + if (answer == null) { + mAnswers.remove(hostname); + } else { + List<InetAddress> answerList = new ArrayList<>(); + for (String addr : answer) { + answerList.add(InetAddresses.parseNumericAddress(addr)); + } + mAnswers.put(hostname, answerList); + } + } + + /** Simulates a getAllByName call for the specified name on the specified mock network. */ + private InetAddress[] getAllByName(Object mock, String hostname) + throws UnknownHostException { + List<InetAddress> answer = getAnswer(mock, hostname); + if (answer == null || answer.size() == 0) { + throw new UnknownHostException(hostname); + } + return answer.toArray(new InetAddress[0]); + } + + /** Starts mocking DNS queries. */ + private void startMocking() throws UnknownHostException { + // Queries on mCleartextDnsNetwork using getAllByName. + doAnswer(invocation -> { + return getAllByName(invocation.getMock(), invocation.getArgument(0)); + }).when(mCleartextDnsNetwork).getAllByName(any()); + + // Queries on mNetwork using getAllByName. + doAnswer(invocation -> { + return getAllByName(invocation.getMock(), invocation.getArgument(0)); + }).when(mNetwork).getAllByName(any()); + + // Queries on mCleartextDnsNetwork using DnsResolver#query. + doAnswer(invocation -> { + String hostname = (String) invocation.getArgument(1); + Executor executor = (Executor) invocation.getArgument(3); + DnsResolver.Callback<List<InetAddress>> callback = invocation.getArgument(5); + + List<InetAddress> answer = getAnswer(invocation.getMock(), hostname); + if (answer != null && answer.size() > 0) { + new Handler(Looper.getMainLooper()).post(() -> { + executor.execute(() -> callback.onAnswer(answer, 0)); + }); + } + // If no answers, do nothing. sendDnsProbeWithTimeout will time out and throw UHE. + return null; + }).when(mDnsResolver).query(any(), any(), anyInt(), any(), any(), any()); + } } + private FakeDns mFakeDns; + @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); - when(mDependencies.getPrivateDnsBypassNetwork(any())).thenReturn(mNetwork); + when(mDependencies.getPrivateDnsBypassNetwork(any())).thenReturn(mCleartextDnsNetwork); when(mDependencies.getDnsResolver()).thenReturn(mDnsResolver); when(mDependencies.getRandom()).thenReturn(mRandom); when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_MODE), anyInt())) @@ -206,7 +271,7 @@ public class NetworkMonitorTest { when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTPS_URL), any())) .thenReturn(TEST_HTTPS_URL); - doReturn(mNetwork).when(mNetwork).getPrivateDnsBypassingCopy(); + doReturn(mCleartextDnsNetwork).when(mNetwork).getPrivateDnsBypassingCopy(); when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)).thenReturn(mCm); when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephony); @@ -222,6 +287,9 @@ public class NetworkMonitorTest { setFallbackSpecs(null); // Test with no fallback spec by default when(mRandom.nextInt()).thenReturn(0); + when(mResources.getInteger(eq(R.integer.config_captive_portal_dns_probe_timeout))) + .thenReturn(500); + doAnswer((invocation) -> { URL url = invocation.getArgument(0); switch(url.toString()) { @@ -237,11 +305,13 @@ public class NetworkMonitorTest { fail("URL not mocked: " + url.toString()); return null; } - }).when(mNetwork).openConnection(any()); + }).when(mCleartextDnsNetwork).openConnection(any()); when(mHttpConnection.getRequestProperties()).thenReturn(new ArrayMap<>()); when(mHttpsConnection.getRequestProperties()).thenReturn(new ArrayMap<>()); - setDnsAnswers(new String[]{"2001:db8::1", "192.0.2.2"}); + mFakeDns = new FakeDns(); + mFakeDns.startMocking(); + mFakeDns.setAnswer("*", new String[]{"2001:db8::1", "192.0.2.2"}); when(mContext.registerReceiver(any(BroadcastReceiver.class), any())).then((invocation) -> { mRegisteredReceivers.add(invocation.getArgument(0)); @@ -264,6 +334,7 @@ public class NetworkMonitorTest { @After public void tearDown() { + mFakeDns.clearAll(); assertTrue(mCreatedNetworkMonitors.size() > 0); // Make a local copy of mCreatedNetworkMonitors because during the iteration below, // WrappedNetworkMonitor#onQuitting will delete elements from it on the handler threads. @@ -284,8 +355,8 @@ public class NetworkMonitorTest { private final ConditionVariable mQuitCv = new ConditionVariable(false); WrappedNetworkMonitor() { - super(mContext, mCallbacks, mNetwork, mLogger, mValidationLogger, mDependencies, - mDataStallStatsUtils); + super(mContext, mCallbacks, mNetwork, mLogger, mValidationLogger, + mDependencies, mDataStallStatsUtils); } @Override @@ -314,23 +385,22 @@ public class NetworkMonitorTest { } } - private WrappedNetworkMonitor makeMonitor() { + private WrappedNetworkMonitor makeMonitor(NetworkCapabilities nc) { final WrappedNetworkMonitor nm = new WrappedNetworkMonitor(); nm.start(); + setNetworkCapabilities(nm, nc); waitForIdle(nm.getHandler()); mCreatedNetworkMonitors.add(nm); return nm; } private WrappedNetworkMonitor makeMeteredNetworkMonitor() { - final WrappedNetworkMonitor nm = makeMonitor(); - setNetworkCapabilities(nm, METERED_CAPABILITIES); + final WrappedNetworkMonitor nm = makeMonitor(METERED_CAPABILITIES); return nm; } private WrappedNetworkMonitor makeNotMeteredNetworkMonitor() { - final WrappedNetworkMonitor nm = makeMonitor(); - setNetworkCapabilities(nm, NOT_METERED_CAPABILITIES); + final WrappedNetworkMonitor nm = makeMonitor(NOT_METERED_CAPABILITIES); return nm; } @@ -595,7 +665,7 @@ public class NetworkMonitorTest { @Test public void testNoInternetCapabilityValidated() throws Exception { runNetworkTest(NO_INTERNET_CAPABILITIES, NETWORK_TEST_RESULT_VALID); - verify(mNetwork, never()).openConnection(any()); + verify(mCleartextDnsNetwork, never()).openConnection(any()); } @Test @@ -603,7 +673,7 @@ public class NetworkMonitorTest { setSslException(mHttpsConnection); setPortal302(mHttpConnection); - final NetworkMonitor nm = makeMonitor(); + final NetworkMonitor nm = makeMonitor(METERED_CAPABILITIES); nm.notifyNetworkConnected(TEST_LINK_PROPERTIES, METERED_CAPABILITIES); verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) @@ -638,6 +708,63 @@ public class NetworkMonitorTest { } @Test + public void testPrivateDnsSuccess() throws Exception { + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + mFakeDns.setAnswer("dns.google", new String[]{"2001:db8::53"}); + + WrappedNetworkMonitor wnm = makeNotMeteredNetworkMonitor(); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0])); + wnm.notifyNetworkConnected(TEST_LINK_PROPERTIES, NOT_METERED_CAPABILITIES); + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .notifyNetworkTested(eq(NETWORK_TEST_RESULT_VALID), eq(null)); + } + + @Test + public void testPrivateDnsResolutionRetryUpdate() throws Exception { + // Set a private DNS hostname that doesn't resolve and expect validation to fail. + mFakeDns.setAnswer("dns.google", new String[0]); + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + + WrappedNetworkMonitor wnm = makeNotMeteredNetworkMonitor(); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0])); + wnm.notifyNetworkConnected(TEST_LINK_PROPERTIES, NOT_METERED_CAPABILITIES); + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .notifyNetworkTested(eq(NETWORK_TEST_RESULT_INVALID), eq(null)); + + // Fix DNS and retry, expect validation to succeed. + reset(mCallbacks); + mFakeDns.setAnswer("dns.google", new String[]{"2001:db8::1"}); + + wnm.forceReevaluation(Process.myUid()); + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .notifyNetworkTested(eq(NETWORK_TEST_RESULT_VALID), eq(null)); + + // Change configuration to an invalid DNS name, expect validation to fail. + reset(mCallbacks); + mFakeDns.setAnswer("dns.bad", new String[0]); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.bad", new InetAddress[0])); + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .notifyNetworkTested(eq(NETWORK_TEST_RESULT_INVALID), eq(null)); + + // Change configuration back to working again, but make private DNS not work. + // Expect validation to fail. + reset(mCallbacks); + mFakeDns.setNonBypassPrivateDnsWorking(false); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0])); + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .notifyNetworkTested(eq(NETWORK_TEST_RESULT_INVALID), eq(null)); + + // Make private DNS work again. Expect validation to succeed. + reset(mCallbacks); + mFakeDns.setNonBypassPrivateDnsWorking(true); + wnm.forceReevaluation(Process.myUid()); + verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) + .notifyNetworkTested(eq(NETWORK_TEST_RESULT_VALID), eq(null)); + } + + @Test public void testDataStall_StallSuspectedAndSendMetrics() throws IOException { WrappedNetworkMonitor wrappedMonitor = makeNotMeteredNetworkMonitor(); wrappedMonitor.setLastProbeTime(SystemClock.elapsedRealtime() - 1000); @@ -728,25 +855,27 @@ public class NetworkMonitorTest { WrappedNetworkMonitor wnm = makeNotMeteredNetworkMonitor(); final int shortTimeoutMs = 200; + // Clear the wildcard DNS response created in setUp. + mFakeDns.setAnswer("*", null); + String[] expected = new String[]{"2001:db8::"}; - setDnsAnswers(expected); + mFakeDns.setAnswer("www.google.com", expected); InetAddress[] actual = wnm.sendDnsProbeWithTimeout("www.google.com", shortTimeoutMs); assertIpAddressArrayEquals(expected, actual); expected = new String[]{"2001:db8::", "192.0.2.1"}; - setDnsAnswers(expected); - actual = wnm.sendDnsProbeWithTimeout("www.google.com", shortTimeoutMs); + mFakeDns.setAnswer("www.googleapis.com", expected); + actual = wnm.sendDnsProbeWithTimeout("www.googleapis.com", shortTimeoutMs); assertIpAddressArrayEquals(expected, actual); - expected = new String[0]; - setDnsAnswers(expected); + mFakeDns.setAnswer("www.google.com", new String[0]); try { wnm.sendDnsProbeWithTimeout("www.google.com", shortTimeoutMs); fail("No DNS results, expected UnknownHostException"); } catch (UnknownHostException e) { } - setDnsAnswers(null); + mFakeDns.setAnswer("www.google.com", null); try { wnm.sendDnsProbeWithTimeout("www.google.com", shortTimeoutMs); fail("DNS query timed out, expected UnknownHostException"); @@ -841,7 +970,7 @@ public class NetworkMonitorTest { } private NetworkMonitor runNetworkTest(NetworkCapabilities nc, int testResult) { - final NetworkMonitor monitor = makeMonitor(); + final NetworkMonitor monitor = makeMonitor(nc); monitor.notifyNetworkConnected(TEST_LINK_PROPERTIES, nc); try { verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)) diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index e02709e10e83..5eaa163db639 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -707,7 +707,9 @@ public class ApplicationsState { private long getTotalInternalSize(PackageStats ps) { if (ps != null) { - return ps.codeSize + ps.dataSize; + // We subtract the cache size because the system can clear it automatically and + // |dataSize| is a superset of |cacheSize|. + return ps.codeSize + ps.dataSize - ps.cacheSize; } return SIZE_INVALID; } @@ -715,7 +717,7 @@ public class ApplicationsState { private long getTotalExternalSize(PackageStats ps) { if (ps != null) { // We also include the cache size here because for non-emulated - // we don't automtically clean cache files. + // we don't automatically clean cache files. return ps.externalCodeSize + ps.externalDataSize + ps.externalCacheSize + ps.externalMediaSize + ps.externalObbSize; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java index b27efd0edc8b..f8697a19c7ab 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/ApplicationsStateRoboTest.java @@ -191,8 +191,9 @@ public class ApplicationsStateRoboTest { shadowContext.setSystemService(Context.STORAGE_STATS_SERVICE, mStorageStatsManager); StorageStats storageStats = new StorageStats(); storageStats.codeBytes = 10; - storageStats.dataBytes = 20; storageStats.cacheBytes = 30; + // Data bytes are a superset of cache bytes. + storageStats.dataBytes = storageStats.cacheBytes + 20; when(mStorageStatsManager.queryStatsForPackage(any(UUID.class), anyString(), any(UserHandle.class))).thenReturn(storageStats); diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 348f01eaa004..715e1ebe31ac 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -229,4 +229,10 @@ <!-- Default for Settings.Secure.AWARE_ENABLED --> <bool name="def_aware_enabled">false</bool> + + <!-- Default for Settings.Secure.SKIP_GESTURE --> + <bool name="def_skip_gesture">false</bool> + + <!-- Default for Settings.Secure.SILENCE_GESTURE --> + <bool name="def_silence_gesture">false</bool> </resources> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 2f3a42fdcc3f..82592ceeb710 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3237,7 +3237,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 178; + private static final int SETTINGS_VERSION = 179; private final int mUserId; @@ -4356,6 +4356,37 @@ public class SettingsProvider extends ContentProvider { currentVersion = 178; } + if (currentVersion == 178) { + // Version 178: Set the default value for Secure Settings: + // SKIP_GESTURE & SILENCE_GESTURE + + final SettingsState secureSettings = getSecureSettingsLocked(userId); + + final Setting skipGesture = secureSettings.getSettingLocked( + Secure.SKIP_GESTURE); + + if (skipGesture.isNull()) { + final boolean defSkipGesture = getContext().getResources().getBoolean( + R.bool.def_skip_gesture); + secureSettings.insertSettingLocked( + Secure.SKIP_GESTURE, defSkipGesture ? "1" : "0", + null, true, SettingsState.SYSTEM_PACKAGE_NAME); + } + + final Setting silenceGesture = secureSettings.getSettingLocked( + Secure.SILENCE_GESTURE); + + if (silenceGesture.isNull()) { + final boolean defSilenceGesture = getContext().getResources().getBoolean( + R.bool.def_silence_gesture); + secureSettings.insertSettingLocked( + Secure.SILENCE_GESTURE, defSilenceGesture ? "1" : "0", + null, true, SettingsState.SYSTEM_PACKAGE_NAME); + } + + currentVersion = 179; + } + // vXXX: Add new settings above this point. diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java index 61aa60bb9675..90fc86bc5b51 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/OverlayPlugin.java @@ -13,18 +13,28 @@ */ package com.android.systemui.plugins; -import com.android.systemui.plugins.annotations.ProvidesInterface; - import android.view.View; +import com.android.systemui.plugins.annotations.ProvidesInterface; + @ProvidesInterface(action = OverlayPlugin.ACTION, version = OverlayPlugin.VERSION) public interface OverlayPlugin extends Plugin { String ACTION = "com.android.systemui.action.PLUGIN_OVERLAY"; - int VERSION = 2; + int VERSION = 3; + /** + * Setup overlay plugin + */ void setup(View statusBar, View navBar); + /** + * Setup overlay plugin with callback + */ + default void setup(View statusBar, View navBar, Callback callback) { + setup(statusBar, navBar); + } + default boolean holdStatusBarOpen() { return false; } @@ -34,4 +44,11 @@ public interface OverlayPlugin extends Plugin { */ default void setCollapseDesired(boolean collapseDesired) { } + + /** + * Used to update system ui whether to hold status bar open + */ + interface Callback { + void onHoldStatusBarOpenChange(); + } } diff --git a/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml b/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml new file mode 100644 index 000000000000..1661bb22d148 --- /dev/null +++ b/packages/SystemUI/res/drawable/bubble_dismiss_circle.xml @@ -0,0 +1,27 @@ +<!-- + 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. +--> +<!-- + The transparent circle outline that encircles the bubbles when they're in the dismiss target. +--> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + + <stroke + android:width="1dp" + android:color="#66FFFFFF" /> + +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/bubble_dismiss_icon.xml b/packages/SystemUI/res/drawable/bubble_dismiss_icon.xml new file mode 100644 index 000000000000..5c8de581f8d1 --- /dev/null +++ b/packages/SystemUI/res/drawable/bubble_dismiss_icon.xml @@ -0,0 +1,26 @@ +<!-- + 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. +--> +<!-- The 'X' bubble dismiss icon. This is just ic_close with a stroke. --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z" + android:fillColor="#FFFFFFFF" + android:strokeColor="#FF000000"/> +</vector> diff --git a/packages/SystemUI/res/drawable/face_dialog_dark_to_checkmark.xml b/packages/SystemUI/res/drawable/face_dialog_dark_to_checkmark.xml index e4ace67577c3..fe1951699eaa 100644 --- a/packages/SystemUI/res/drawable/face_dialog_dark_to_checkmark.xml +++ b/packages/SystemUI/res/drawable/face_dialog_dark_to_checkmark.xml @@ -40,7 +40,7 @@ <path android:name="_R_G_L_0_G_D_0_P_0" android:fillAlpha="0" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M-116 -16.5 C-116,-16.5 -31.25,68.5 -31.25,68.5 C-31.25,68.5 108.75,-71.5 108.75,-71.5 " android:trimPathStart="0" @@ -58,7 +58,7 @@ android:pathData=" M-116 -16.5 C-116,-16.5 -31.25,68.5 -31.25,68.5 C-31.25,68.5 108.75,-71.5 108.75,-71.5 " android:strokeWidth="20" android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:trimPathStart="0" android:trimPathEnd="0" android:trimPathOffset="0" /> @@ -68,7 +68,7 @@ android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c " android:strokeWidth="2.5" android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> @@ -85,7 +85,7 @@ android:pathData=" M4.71 1.1 C3.71,2.12 2.32,2.75 0.79,2.75 C-2.25,2.75 -4.7,0.29 -4.7,-2.75 " android:strokeWidth="2" android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> @@ -99,7 +99,7 @@ <path android:name="_R_G_L_0_G_D_4_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M-2.1 0 C-2.1,1.2 -1.2,2.1 0,2.1 C1.1,2.1 2.1,1.2 2.1,0 C2.1,-1.2 1.2,-2.1 0,-2.1 C-1.2,-2.1 -2.1,-1.2 -2.1,0c " /> </group> @@ -112,7 +112,7 @@ <path android:name="_R_G_L_0_G_D_5_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M-2.1 0 C-2.1,1.2 -1.2,2.1 0,2.1 C1.2,2.1 2.1,1.2 2.1,0 C2.1,-1.2 1.2,-2.1 0,-2.1 C-1.2,-2.1 -2.1,-1.2 -2.1,0c " /> </group> @@ -125,7 +125,7 @@ <path android:name="_R_G_L_0_G_D_6_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M2.6 3.25 C2.6,3.25 -2.6,3.25 -2.6,3.25 C-2.6,3.25 -2.6,1.25 -2.6,1.25 C-2.6,1.25 0.6,1.25 0.6,1.25 C0.6,1.25 0.6,-3.25 0.6,-3.25 C0.6,-3.25 2.6,-3.25 2.6,-3.25 C2.6,-3.25 2.6,3.25 2.6,3.25c " /> </group> @@ -386,8 +386,8 @@ android:duration="67" android:propertyName="strokeColor" android:startOffset="0" - android:valueFrom="?android:attr/colorAccent" - android:valueTo="?android:attr/colorAccent" + android:valueFrom="@color/biometric_dialog_accent" + android:valueTo="@color/biometric_dialog_accent" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -397,8 +397,8 @@ android:duration="17" android:propertyName="strokeColor" android:startOffset="67" - android:valueFrom="?android:attr/colorAccent" - android:valueTo="?android:attr/colorAccent" + android:valueFrom="@color/biometric_dialog_accent" + android:valueTo="@color/biometric_dialog_accent" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> diff --git a/packages/SystemUI/res/drawable/face_dialog_dark_to_error.xml b/packages/SystemUI/res/drawable/face_dialog_dark_to_error.xml index a96d21addb2b..0c05019bf199 100644 --- a/packages/SystemUI/res/drawable/face_dialog_dark_to_error.xml +++ b/packages/SystemUI/res/drawable/face_dialog_dark_to_error.xml @@ -36,7 +36,7 @@ android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c " android:strokeWidth="2.5" android:strokeAlpha="1" - android:strokeColor="@color/biometric_face_icon_gray" + android:strokeColor="@color/biometric_dialog_gray" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> @@ -45,7 +45,7 @@ android:pathData=" M33.75 42.75 C32.75,43.76 31.37,44.39 29.83,44.39 C26.8,44.39 24.34,41.93 24.34,38.9 " android:strokeWidth="2" android:strokeAlpha="1" - android:strokeColor="@color/biometric_face_icon_gray" + android:strokeColor="@color/biometric_dialog_gray" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> @@ -58,7 +58,7 @@ <path android:name="_R_G_L_0_G_D_2_P_0" android:fillAlpha="1" - android:fillColor="@color/biometric_face_icon_gray" + android:fillColor="@color/biometric_dialog_gray" android:fillType="nonZero" android:pathData=" M-2.1 0 C-2.1,1.2 -1.2,2.1 0,2.1 C1.1,2.1 2.1,1.2 2.1,0 C2.1,-1.2 1.2,-2.1 0,-2.1 C-1.2,-2.1 -2.1,-1.2 -2.1,0c " /> </group> @@ -71,7 +71,7 @@ <path android:name="_R_G_L_0_G_D_3_P_0" android:fillAlpha="1" - android:fillColor="@color/biometric_face_icon_gray" + android:fillColor="@color/biometric_dialog_gray" android:fillType="nonZero" android:pathData=" M-2.1 0 C-2.1,1.2 -1.2,2.1 0,2.1 C1.2,2.1 2.1,1.2 2.1,0 C2.1,-1.2 1.2,-2.1 0,-2.1 C-1.2,-2.1 -2.1,-1.2 -2.1,0c " /> </group> @@ -82,7 +82,7 @@ <path android:name="_R_G_L_0_G_D_4_P_0" android:fillAlpha="1" - android:fillColor="@color/biometric_face_icon_gray" + android:fillColor="@color/biometric_dialog_gray" android:fillType="nonZero" android:pathData=" M2.6 3.25 C2.6,3.25 -2.6,3.25 -2.6,3.25 C-2.6,3.25 -2.6,1.25 -2.6,1.25 C-2.6,1.25 0.6,1.25 0.6,1.25 C0.6,1.25 0.6,-3.25 0.6,-3.25 C0.6,-3.25 2.6,-3.25 2.6,-3.25 C2.6,-3.25 2.6,3.25 2.6,3.25c " /> </group> @@ -99,8 +99,8 @@ android:duration="50" android:propertyName="strokeColor" android:startOffset="0" - android:valueFrom="@color/biometric_face_icon_gray" - android:valueTo="@color/biometric_face_icon_gray" + android:valueFrom="@color/biometric_dialog_gray" + android:valueTo="@color/biometric_dialog_gray" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -110,8 +110,8 @@ android:duration="17" android:propertyName="strokeColor" android:startOffset="50" - android:valueFrom="@color/biometric_face_icon_gray" - android:valueTo="?android:attr/colorError" + android:valueFrom="@color/biometric_dialog_gray" + android:valueTo="@color/biometric_dialog_error" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -127,8 +127,8 @@ android:duration="50" android:propertyName="strokeColor" android:startOffset="0" - android:valueFrom="@color/biometric_face_icon_gray" - android:valueTo="@color/biometric_face_icon_gray" + android:valueFrom="@color/biometric_dialog_gray" + android:valueTo="@color/biometric_dialog_gray" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -138,8 +138,8 @@ android:duration="17" android:propertyName="strokeColor" android:startOffset="50" - android:valueFrom="@color/biometric_face_icon_gray" - android:valueTo="?android:attr/colorError" + android:valueFrom="@color/biometric_dialog_gray" + android:valueTo="@color/biometric_dialog_error" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -369,8 +369,8 @@ android:duration="50" android:propertyName="fillColor" android:startOffset="0" - android:valueFrom="@color/biometric_face_icon_gray" - android:valueTo="@color/biometric_face_icon_gray" + android:valueFrom="@color/biometric_dialog_gray" + android:valueTo="@color/biometric_dialog_gray" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -380,8 +380,8 @@ android:duration="17" android:propertyName="fillColor" android:startOffset="50" - android:valueFrom="@color/biometric_face_icon_gray" - android:valueTo="?android:attr/colorError" + android:valueFrom="@color/biometric_dialog_gray" + android:valueTo="@color/biometric_dialog_error" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> diff --git a/packages/SystemUI/res/drawable/face_dialog_error_to_idle.xml b/packages/SystemUI/res/drawable/face_dialog_error_to_idle.xml index aca12fce8d2f..d3cee25a2146 100644 --- a/packages/SystemUI/res/drawable/face_dialog_error_to_idle.xml +++ b/packages/SystemUI/res/drawable/face_dialog_error_to_idle.xml @@ -29,7 +29,7 @@ android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c " android:strokeWidth="2.5" android:strokeAlpha="1" - android:strokeColor="?android:attr/colorError" + android:strokeColor="@color/biometric_dialog_error" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> @@ -38,7 +38,7 @@ android:pathData=" M34.78 38.76 C33.83,38.75 31.54,38.75 30.01,38.75 C26.97,38.75 26.14,38.75 24.3,38.76 " android:strokeWidth="2.5" android:strokeAlpha="1" - android:strokeColor="?android:attr/colorError" + android:strokeColor="@color/biometric_dialog_error" android:trimPathStart="0.34" android:trimPathEnd="0.5700000000000001" android:trimPathOffset="0" /> @@ -51,7 +51,7 @@ <path android:name="_R_G_L_0_G_D_2_P_0" android:fillAlpha="0" - android:fillColor="@color/biometric_face_icon_gray" + android:fillColor="@color/biometric_dialog_gray" android:fillType="nonZero" android:pathData=" M-2.1 0 C-2.1,1.2 -1.2,2.1 0,2.1 C1.1,2.1 2.1,1.2 2.1,0 C2.1,-1.2 1.2,-2.1 0,-2.1 C-1.2,-2.1 -2.1,-1.2 -2.1,0c " /> </group> @@ -64,7 +64,7 @@ <path android:name="_R_G_L_0_G_D_3_P_0" android:fillAlpha="0" - android:fillColor="@color/biometric_face_icon_gray" + android:fillColor="@color/biometric_dialog_gray" android:fillType="nonZero" android:pathData=" M-2.1 0 C-2.1,1.2 -1.2,2.1 0,2.1 C1.2,2.1 2.1,1.2 2.1,0 C2.1,-1.2 1.2,-2.1 0,-2.1 C-1.2,-2.1 -2.1,-1.2 -2.1,0c " /> </group> @@ -75,7 +75,7 @@ <path android:name="_R_G_L_0_G_D_4_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorError" + android:fillColor="@color/biometric_dialog_error" android:fillType="nonZero" android:pathData=" M0.9 3.25 C0.9,3.25 -1.5,3.25 -1.5,3.25 C-1.5,3.25 -1.5,1.25 -1.5,1.25 C-1.5,1.25 -1.5,1.25 -1.5,1.25 C-1.5,1.25 -1.5,-11.71 -1.5,-11.71 C-1.5,-11.71 0.9,-11.71 0.9,-11.71 C0.9,-11.71 0.9,3.25 0.9,3.25c " /> </group> @@ -91,8 +91,8 @@ android:duration="83" android:propertyName="strokeColor" android:startOffset="0" - android:valueFrom="?android:attr/colorError" - android:valueTo="?android:attr/colorError" + android:valueFrom="@color/biometric_dialog_error" + android:valueTo="@color/biometric_dialog_error" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -102,8 +102,8 @@ android:duration="17" android:propertyName="strokeColor" android:startOffset="83" - android:valueFrom="?android:attr/colorError" - android:valueTo="@color/biometric_face_icon_gray" + android:valueFrom="@color/biometric_dialog_error" + android:valueTo="@color/biometric_dialog_gray" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -119,8 +119,8 @@ android:duration="83" android:propertyName="strokeColor" android:startOffset="0" - android:valueFrom="?android:attr/colorError" - android:valueTo="?android:attr/colorError" + android:valueFrom="@color/biometric_dialog_error" + android:valueTo="@color/biometric_dialog_error" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -130,8 +130,8 @@ android:duration="17" android:propertyName="strokeColor" android:startOffset="83" - android:valueFrom="?android:attr/colorError" - android:valueTo="@color/biometric_face_icon_gray" + android:valueFrom="@color/biometric_dialog_error" + android:valueTo="@color/biometric_dialog_gray" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -427,8 +427,8 @@ android:duration="83" android:propertyName="fillColor" android:startOffset="0" - android:valueFrom="?android:attr/colorError" - android:valueTo="?android:attr/colorError" + android:valueFrom="@color/biometric_dialog_error" + android:valueTo="@color/biometric_dialog_error" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> @@ -438,8 +438,8 @@ android:duration="17" android:propertyName="fillColor" android:startOffset="83" - android:valueFrom="?android:attr/colorError" - android:valueTo="@color/biometric_face_icon_gray" + android:valueFrom="@color/biometric_dialog_error" + android:valueTo="@color/biometric_dialog_gray" android:valueType="colorType"> <aapt:attr name="android:interpolator"> <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> diff --git a/packages/SystemUI/res/drawable/face_dialog_pulse_dark_to_light.xml b/packages/SystemUI/res/drawable/face_dialog_pulse_dark_to_light.xml index 0de856c28dcd..427be1447679 100644 --- a/packages/SystemUI/res/drawable/face_dialog_pulse_dark_to_light.xml +++ b/packages/SystemUI/res/drawable/face_dialog_pulse_dark_to_light.xml @@ -18,7 +18,7 @@ android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c " android:strokeWidth="2.5" android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> @@ -27,26 +27,26 @@ android:pathData=" M33.75 42.75 C32.75,43.77 31.37,44.39 29.83,44.39 C26.8,44.39 24.34,41.93 24.34,38.9 " android:strokeWidth="2" android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> <path android:name="_R_G_L_0_G_D_2_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M39 23.8 C39,25 39.9,25.9 41.1,25.9 C42.2,25.9 43.2,25 43.2,23.8 C43.2,22.6 42.3,21.7 41.1,21.7 C39.9,21.7 39,22.6 39,23.8c " /> <path android:name="_R_G_L_0_G_D_3_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M16.5 23.8 C16.5,25 17.4,25.9 18.6,25.9 C19.8,25.9 20.7,25 20.7,23.8 C20.7,22.6 19.8,21.7 18.6,21.7 C17.4,21.7 16.5,22.6 16.5,23.8c " /> <path android:name="_R_G_L_0_G_D_4_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M33.33 34.95 C33.33,34.95 28.13,34.95 28.13,34.95 C28.13,34.95 28.13,32.95 28.13,32.95 C28.13,32.95 31.33,32.95 31.33,32.95 C31.33,32.95 31.33,28.45 31.33,28.45 C31.33,28.45 33.33,28.45 33.33,28.45 C33.33,28.45 33.33,34.95 33.33,34.95c " /> </group> diff --git a/packages/SystemUI/res/drawable/face_dialog_pulse_light_to_dark.xml b/packages/SystemUI/res/drawable/face_dialog_pulse_light_to_dark.xml index 31a0cbb2605c..ab26408a2ed6 100644 --- a/packages/SystemUI/res/drawable/face_dialog_pulse_light_to_dark.xml +++ b/packages/SystemUI/res/drawable/face_dialog_pulse_light_to_dark.xml @@ -18,7 +18,7 @@ android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c " android:strokeWidth="2.5" android:strokeAlpha="0.5" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> @@ -27,26 +27,26 @@ android:pathData=" M33.75 42.75 C32.75,43.77 31.37,44.39 29.83,44.39 C26.8,44.39 24.34,41.93 24.34,38.9 " android:strokeWidth="2" android:strokeAlpha="0.5" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> <path android:name="_R_G_L_0_G_D_2_P_0" android:fillAlpha="0.5" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M39 23.8 C39,25 39.9,25.9 41.1,25.9 C42.2,25.9 43.2,25 43.2,23.8 C43.2,22.6 42.3,21.7 41.1,21.7 C39.9,21.7 39,22.6 39,23.8c " /> <path android:name="_R_G_L_0_G_D_3_P_0" android:fillAlpha="0.5" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M16.5 23.8 C16.5,25 17.4,25.9 18.6,25.9 C19.8,25.9 20.7,25 20.7,23.8 C20.7,22.6 19.8,21.7 18.6,21.7 C17.4,21.7 16.5,22.6 16.5,23.8c " /> <path android:name="_R_G_L_0_G_D_4_P_0" android:fillAlpha="0.5" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M33.33 34.95 C33.33,34.95 28.13,34.95 28.13,34.95 C28.13,34.95 28.13,32.95 28.13,32.95 C28.13,32.95 31.33,32.95 31.33,32.95 C31.33,32.95 31.33,28.45 31.33,28.45 C31.33,28.45 33.33,28.45 33.33,28.45 C33.33,28.45 33.33,34.95 33.33,34.95c " /> </group> diff --git a/packages/SystemUI/res/drawable/face_dialog_wink_from_dark.xml b/packages/SystemUI/res/drawable/face_dialog_wink_from_dark.xml index adbe446abb9d..0cd542d51b51 100644 --- a/packages/SystemUI/res/drawable/face_dialog_wink_from_dark.xml +++ b/packages/SystemUI/res/drawable/face_dialog_wink_from_dark.xml @@ -13,7 +13,7 @@ android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c " android:strokeWidth="2.5" android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> @@ -30,14 +30,14 @@ android:pathData=" M33.75 42.75 C32.75,43.77 31.37,44.39 29.83,44.39 C26.8,44.39 24.34,41.93 24.34,38.9 " android:strokeWidth="2" android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:trimPathStart="0" android:trimPathEnd="1" android:trimPathOffset="0" /> <path android:name="_R_G_L_0_G_D_1_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M39 23.8 C39,25 39.9,25.9 41.1,25.9 C42.2,25.9 43.2,25 43.2,23.8 C43.2,22.6 42.3,21.7 41.1,21.7 C39.9,21.7 39,22.6 39,23.8c " /> <group @@ -49,14 +49,14 @@ <path android:name="_R_G_L_0_G_D_2_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M-2.1 0 C-2.1,1.2 -1.2,2.1 0,2.1 C1.2,2.1 2.1,1.2 2.1,0 C2.1,-1.2 1.2,-2.1 0,-2.1 C-1.2,-2.1 -2.1,-1.2 -2.1,0c " /> </group> <path android:name="_R_G_L_0_G_D_3_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorAccent" + android:fillColor="@color/biometric_dialog_accent" android:fillType="nonZero" android:pathData=" M33.33 34.95 C33.33,34.95 28.13,34.95 28.13,34.95 C28.13,34.95 28.13,32.95 28.13,32.95 C28.13,32.95 31.33,32.95 31.33,32.95 C31.33,32.95 31.33,28.45 31.33,28.45 C31.33,28.45 33.33,28.45 33.33,28.45 C33.33,28.45 33.33,34.95 33.33,34.95c " /> </group> diff --git a/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_fp.xml b/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_fp.xml index 8f411f4c07fe..33263a9131a0 100644 --- a/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_fp.xml +++ b/packages/SystemUI/res/drawable/fingerprint_dialog_error_to_fp.xml @@ -39,7 +39,7 @@ android:name="_R_G_L_1_G_D_0_P_0" android:pathData=" M79.63 67.24 C79.63,67.24 111.5,47.42 147.83,67.24 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -50,7 +50,7 @@ android:name="_R_G_L_1_G_D_1_P_0" android:pathData=" M64.27 98.07 C64.27,98.07 80.13,73.02 113.98,73.02 C147.83,73.02 163.56,97.26 163.56,97.26 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -61,7 +61,7 @@ android:name="_R_G_L_1_G_D_2_P_0" android:pathData=" M72.53 151.07 C72.53,151.07 62.46,122.89 76.16,105.55 C89.86,88.21 106.72,86.73 113.98,86.73 C121.08,86.73 153.51,90.62 158.7,125.87 C159.14,128.82 158.8,132.88 157.18,136.09 C154.88,140.63 150.62,143.63 145.85,143.97 C133.78,144.85 129.76,137.92 129.26,128.49 C128.88,121.19 122.49,115.35 113.15,115.35 C102.91,115.35 95.97,126.69 99.77,139.74 C103.57,152.78 111.33,163.85 130.32,169.13 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -72,7 +72,7 @@ android:name="_R_G_L_1_G_D_3_P_0" android:pathData=" M100.6 167.84 C100.6,167.84 82.76,152.1 83.75,130.31 C84.75,108.53 102.58,100.7 113.73,100.7 C124.87,100.7 144.19,108.56 144.19,130.01 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -83,7 +83,7 @@ android:name="_R_G_L_1_G_D_4_P_0" android:pathData=" M113.73 129.17 C113.73,129.17 113.15,161.33 149.15,156.58 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -109,7 +109,7 @@ <path android:name="_R_G_L_0_G_D_0_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorError" + android:fillColor="@color/biometric_dialog_error" android:fillType="nonZero" android:pathData=" M-1.2 -1.25 C-1.2,-1.25 1.2,-1.25 1.2,-1.25 C1.2,-1.25 1.2,1.25 1.2,1.25 C1.2,1.25 -1.2,1.25 -1.2,1.25 C-1.2,1.25 -1.2,-1.25 -1.2,-1.25c " /> </group> @@ -124,7 +124,7 @@ <path android:name="_R_G_L_0_G_D_1_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorError" + android:fillColor="@color/biometric_dialog_error" android:fillType="nonZero" android:pathData=" M-1.2 -7.5 C-1.2,-7.5 1.2,-7.5 1.2,-7.5 C1.2,-7.5 1.2,7.5 1.2,7.5 C1.2,7.5 -1.2,7.5 -1.2,7.5 C-1.2,7.5 -1.2,-7.5 -1.2,-7.5c " /> </group> @@ -132,7 +132,7 @@ android:name="_R_G_L_0_G_D_2_P_0" android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorError" + android:strokeColor="@color/biometric_dialog_error" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2.5" diff --git a/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_error.xml b/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_error.xml index 89b822840b14..b899828cd85c 100644 --- a/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_error.xml +++ b/packages/SystemUI/res/drawable/fingerprint_dialog_fp_to_error.xml @@ -39,7 +39,7 @@ android:name="_R_G_L_1_G_D_0_P_0" android:pathData=" M79.63 67.24 C79.63,67.24 111.5,47.42 147.83,67.24 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -50,7 +50,7 @@ android:name="_R_G_L_1_G_D_1_P_0" android:pathData=" M64.27 98.07 C64.27,98.07 80.13,73.02 113.98,73.02 C147.83,73.02 163.56,97.26 163.56,97.26 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -61,7 +61,7 @@ android:name="_R_G_L_1_G_D_2_P_0" android:pathData=" M72.53 151.07 C72.53,151.07 62.46,122.89 76.16,105.55 C89.86,88.21 106.72,86.73 113.98,86.73 C121.08,86.73 153.51,90.62 158.7,125.87 C159.14,128.82 158.8,132.88 157.18,136.09 C154.88,140.63 150.62,143.63 145.85,143.97 C133.78,144.85 129.76,137.92 129.26,128.49 C128.88,121.19 122.49,115.35 113.15,115.35 C102.91,115.35 95.97,126.69 99.77,139.74 C103.57,152.78 111.33,163.85 130.32,169.13 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -72,7 +72,7 @@ android:name="_R_G_L_1_G_D_3_P_0" android:pathData=" M100.6 167.84 C100.6,167.84 82.76,152.1 83.75,130.31 C84.75,108.53 102.58,100.7 113.73,100.7 C124.87,100.7 144.19,108.56 144.19,130.01 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -83,7 +83,7 @@ android:name="_R_G_L_1_G_D_4_P_0" android:pathData=" M113.73 129.17 C113.73,129.17 113.15,161.33 149.15,156.58 " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorAccent" + android:strokeColor="@color/biometric_dialog_accent" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="5.5" @@ -109,7 +109,7 @@ <path android:name="_R_G_L_0_G_D_0_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorError" + android:fillColor="@color/biometric_dialog_error" android:fillType="nonZero" android:pathData=" M-1.2 -1.25 C-1.2,-1.25 1.2,-1.25 1.2,-1.25 C1.2,-1.25 1.2,1.25 1.2,1.25 C1.2,1.25 -1.2,1.25 -1.2,1.25 C-1.2,1.25 -1.2,-1.25 -1.2,-1.25c " /> </group> @@ -124,7 +124,7 @@ <path android:name="_R_G_L_0_G_D_1_P_0" android:fillAlpha="1" - android:fillColor="?android:attr/colorError" + android:fillColor="@color/biometric_dialog_error" android:fillType="nonZero" android:pathData=" M-1.2 -7.5 C-1.2,-7.5 1.2,-7.5 1.2,-7.5 C1.2,-7.5 1.2,7.5 1.2,7.5 C1.2,7.5 -1.2,7.5 -1.2,7.5 C-1.2,7.5 -1.2,-7.5 -1.2,-7.5c " /> </group> @@ -132,7 +132,7 @@ android:name="_R_G_L_0_G_D_2_P_0" android:pathData=" M30 6.2 C16.9,6.2 6.3,16.8 6.3,30 C6.3,43.2 16.9,53.8 30,53.8 C43.1,53.8 53.8,43.2 53.8,30 C53.8,16.8 43.1,6.2 30,6.2c " android:strokeAlpha="1" - android:strokeColor="?android:attr/colorError" + android:strokeColor="@color/biometric_dialog_error" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2.5" diff --git a/packages/SystemUI/res/layout/biometric_dialog.xml b/packages/SystemUI/res/layout/biometric_dialog.xml index c452855cc9e9..1abb8735ddab 100644 --- a/packages/SystemUI/res/layout/biometric_dialog.xml +++ b/packages/SystemUI/res/layout/biometric_dialog.xml @@ -119,7 +119,7 @@ android:textSize="12sp" android:gravity="center_horizontal" android:accessibilityLiveRegion="polite" - android:textColor="?android:attr/textColorSecondary"/> + android:textColor="@color/biometric_dialog_gray"/> <LinearLayout android:layout_width="match_parent" diff --git a/packages/SystemUI/res/layout/bubble_dismiss_target.xml b/packages/SystemUI/res/layout/bubble_dismiss_target.xml new file mode 100644 index 000000000000..245177c8461b --- /dev/null +++ b/packages/SystemUI/res/layout/bubble_dismiss_target.xml @@ -0,0 +1,66 @@ +<!-- + ~ 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 + --> +<!-- Bubble dismiss target consisting of an X icon and the text 'Dismiss'. --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="@dimen/pip_dismiss_gradient_height" + android:layout_gravity="bottom|center_horizontal"> + + <LinearLayout + android:id="@+id/bubble_dismiss_icon_container" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center" + android:paddingBottom="@dimen/bubble_dismiss_target_padding_y" + android:paddingTop="@dimen/bubble_dismiss_target_padding_y" + android:paddingLeft="@dimen/bubble_dismiss_target_padding_x" + android:paddingRight="@dimen/bubble_dismiss_target_padding_x" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="horizontal"> + + <ImageView + android:id="@+id/bubble_dismiss_close_icon" + android:layout_width="24dp" + android:layout_height="24dp" + android:src="@drawable/bubble_dismiss_icon" /> + + <TextView + android:id="@+id/bubble_dismiss_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="9dp" + android:layout_marginBottom="9dp" + android:layout_marginLeft="8dp" + android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Body1" + android:textColor="@android:color/white" + android:shadowColor="@android:color/black" + android:shadowDx="-1" + android:shadowDy="1" + android:shadowRadius="0.01" + android:text="@string/bubble_dismiss_text" /> + + </LinearLayout> + + <FrameLayout + android:id="@+id/bubble_dismiss_circle" + android:layout_width="@dimen/bubble_dismiss_encircle_size" + android:layout_height="@dimen/bubble_dismiss_encircle_size" + android:layout_gravity="center" + android:alpha="0" + android:background="@drawable/bubble_dismiss_circle" /> +</FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index 07d81c037e21..a53855825105 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -68,4 +68,10 @@ <!-- The color of the text in the Global Actions menu --> <color name="global_actions_alert_text">@color/GM2_red_300</color> + + <!-- Biometric dialog colors --> + <color name="biometric_dialog_gray">#ff888888</color> + <color name="biometric_dialog_accent">#ff80cbc4</color> <!-- light teal --> + <color name="biometric_dialog_error">#fff28b82</color> <!-- red 300 --> + </resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 2200cf0112ac..14a120b36279 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -157,7 +157,9 @@ <!-- Biometric dialog colors --> <color name="biometric_dialog_dim_color">#80000000</color> <!-- 50% black --> - <color name="biometric_face_icon_gray">#ff757575</color> + <color name="biometric_dialog_gray">#ff757575</color> + <color name="biometric_dialog_accent">#ff008577</color> <!-- dark teal --> + <color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 --> <!-- Logout button --> <color name="logout_button_bg_color">#ccffffff</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 0fa542c498bc..e53798d083fc 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1095,6 +1095,8 @@ <dimen name="bubble_padding">8dp</dimen> <!-- Size of individual bubbles. --> <dimen name="individual_bubble_size">52dp</dimen> + <!-- Size of the circle around the bubbles when they're in the dismiss target. --> + <dimen name="bubble_dismiss_encircle_size">56dp</dimen> <!-- How much to inset the icon in the circle --> <dimen name="bubble_icon_inset">16dp</dimen> <!-- Padding around the view displayed when the bubble is expanded --> @@ -1131,7 +1133,12 @@ <dimen name="bubble_header_icon_size">48dp</dimen> <!-- Space between the pointer triangle and the bubble expanded view --> <dimen name="bubble_pointer_margin">8dp</dimen> - + <!-- Height of the permission prompt shown with bubbles --> + <dimen name="bubble_permission_height">120dp</dimen> + <!-- Padding applied to the bubble dismiss target. Touches in this padding cause the bubbles to + snap to the dismiss target. --> + <dimen name="bubble_dismiss_target_padding_x">40dp</dimen> + <dimen name="bubble_dismiss_target_padding_y">20dp</dimen> <!-- Size of the RAT type for CellularTile --> <dimen name="celltile_rat_type_size">10sp</dimen> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6ba72b6b85ad..13a20bf9bb7c 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2442,4 +2442,6 @@ <string name="bubble_accessibility_action_move_bottom_left">Move bottom left</string> <!-- Action in accessibility menu to move the stack of bubbles to the bottom right of the screen. [CHAR LIMIT=30]--> <string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string> + <!-- Text used for the bubble dismiss area. Bubbles dragged to, or flung towards, this area will go away. [CHAR LIMIT=20] --> + <string name="bubble_dismiss_text">Dismiss</string> </resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java index 814db19c56b7..06ae399a5e4e 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java @@ -354,6 +354,7 @@ public class TaskStackChangeListeners extends TaskStackListener { mTaskStackListeners.get(i).onBackPressedOnTaskRoot( (RunningTaskInfo) msg.obj); } + break; } } } diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 12f40f385cb2..61a0f72315ea 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -205,7 +205,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv final Handler mainHandler = new Handler(Looper.getMainLooper()); Dependency.get(PluginManager.class).addPluginListener( new PluginListener<OverlayPlugin>() { - private ArraySet<OverlayPlugin> mOverlays; + private ArraySet<OverlayPlugin> mOverlays = new ArraySet<>(); @Override public void onPluginConnected(OverlayPlugin plugin, Context pluginContext) { @@ -215,18 +215,7 @@ public class SystemUIApplication extends Application implements SysUiServiceProv StatusBar statusBar = getComponent(StatusBar.class); if (statusBar != null) { plugin.setup(statusBar.getStatusBarWindow(), - statusBar.getNavigationBarView()); - } - // Lazy init. - if (mOverlays == null) mOverlays = new ArraySet<>(); - if (plugin.holdStatusBarOpen()) { - mOverlays.add(plugin); - Dependency.get(StatusBarWindowController.class) - .setStateListener(b -> mOverlays.forEach( - o -> o.setCollapseDesired(b))); - Dependency.get(StatusBarWindowController.class) - .setForcePluginOpen(mOverlays.size() != 0); - + statusBar.getNavigationBarView(), new Callback(plugin)); } } }); @@ -243,6 +232,33 @@ public class SystemUIApplication extends Application implements SysUiServiceProv } }); } + + class Callback implements OverlayPlugin.Callback { + private final OverlayPlugin mPlugin; + + Callback(OverlayPlugin plugin) { + mPlugin = plugin; + } + + @Override + public void onHoldStatusBarOpenChange() { + if (mPlugin.holdStatusBarOpen()) { + mOverlays.add(mPlugin); + } else { + mOverlays.remove(mPlugin); + } + mainHandler.post(new Runnable() { + @Override + public void run() { + Dependency.get(StatusBarWindowController.class) + .setStateListener(b -> mOverlays.forEach( + o -> o.setCollapseDesired(b))); + Dependency.get(StatusBarWindowController.class) + .setForcePluginOpen(mOverlays.size() != 0); + } + }); + } + } }, OverlayPlugin.class, true /* Allow multiple plugins */); mServicesStarted = true; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java index f17fcbab31c4..f25b580b13a3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricDialogView.java @@ -18,7 +18,6 @@ package com.android.systemui.biometrics; import android.app.admin.DevicePolicyManager; import android.content.Context; -import android.content.res.TypedArray; import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; @@ -93,6 +92,7 @@ public abstract class BiometricDialogView extends LinearLayout { protected final int mTextColor; private Bundle mBundle; + private Bundle mRestoredState; private int mState; private boolean mAnimatingAway; @@ -151,12 +151,8 @@ public abstract class BiometricDialogView extends LinearLayout { mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); mAnimationTranslationOffset = getResources() .getDimension(R.dimen.biometric_dialog_animation_translation_offset); - - TypedArray array = getContext().obtainStyledAttributes( - new int[]{android.R.attr.colorError, android.R.attr.textColorSecondary}); - mErrorColor = array.getColor(0, 0); - mTextColor = array.getColor(1, 0); - array.recycle(); + mErrorColor = getResources().getColor(R.color.biometric_dialog_error); + mTextColor = getResources().getColor(R.color.biometric_dialog_gray); DisplayMetrics metrics = new DisplayMetrics(); mWindowManager.getDefaultDisplay().getMetrics(metrics); @@ -225,7 +221,6 @@ public abstract class BiometricDialogView extends LinearLayout { mTryAgainButton.setOnClickListener((View v) -> { updateState(STATE_AUTHENTICATING); showTryAgainButton(false /* show */); - handleClearMessage(); mCallback.onTryAgainPressed(); }); @@ -292,7 +287,7 @@ public abstract class BiometricDialogView extends LinearLayout { mNegativeButton.setText(mBundle.getCharSequence(BiometricPrompt.KEY_NEGATIVE_TEXT)); - if (requiresConfirmation()) { + if (requiresConfirmation() && mRestoredState == null) { mPositiveButton.setVisibility(View.VISIBLE); mPositiveButton.setEnabled(false); } @@ -449,6 +444,7 @@ public abstract class BiometricDialogView extends LinearLayout { if (newState == STATE_PENDING_CONFIRMATION) { mHandler.removeMessages(MSG_CLEAR_MESSAGE); mErrorText.setVisibility(View.INVISIBLE); + mPositiveButton.setVisibility(View.VISIBLE); mPositiveButton.setEnabled(true); } else if (newState == STATE_AUTHENTICATED) { mPositiveButton.setVisibility(View.GONE); @@ -471,6 +467,7 @@ public abstract class BiometricDialogView extends LinearLayout { } public void restoreState(Bundle bundle) { + mRestoredState = bundle; mTryAgainButton.setVisibility(bundle.getInt(KEY_TRY_AGAIN_VISIBILITY)); mPositiveButton.setVisibility(bundle.getInt(KEY_CONFIRM_VISIBILITY)); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java b/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java index 9a0b1906dd4a..9679d26d4bfe 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceDialogView.java @@ -360,8 +360,6 @@ public class FaceDialogView extends BiometricDialogView { if (show) { mPositiveButton.setVisibility(View.GONE); - } else if (!show && requiresConfirmation()) { - mPositiveButton.setVisibility(View.VISIBLE); } } @@ -402,6 +400,12 @@ public class FaceDialogView extends BiometricDialogView { } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATING) { mHandler.removeCallbacks(mErrorToIdleAnimationRunnable); mIconController.startPulsing(); + } else if (oldState == STATE_ERROR && newState == STATE_PENDING_CONFIRMATION) { + mHandler.removeCallbacks(mErrorToIdleAnimationRunnable); + mIconController.animateOnce(R.drawable.face_dialog_wink_from_dark); + } else if (oldState == STATE_ERROR && newState == STATE_AUTHENTICATED) { + mHandler.removeCallbacks(mErrorToIdleAnimationRunnable); + mIconController.animateOnce(R.drawable.face_dialog_dark_to_checkmark); } else if (oldState == STATE_AUTHENTICATING && newState == STATE_ERROR) { mIconController.animateOnce(R.drawable.face_dialog_dark_to_error); mHandler.postDelayed(mErrorToIdleAnimationRunnable, BiometricPrompt.HIDE_DIALOG_DELAY); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index 7094d28c29f5..ac4a93ba7fb0 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -46,7 +46,7 @@ class Bubble { private long mLastUpdated; private long mLastAccessed; - private static String groupId(NotificationEntry entry) { + public static String groupId(NotificationEntry entry) { UserHandle user = entry.notification.getUser(); return user.getIdentifier() + "|" + entry.notification.getPackageName(); } @@ -120,11 +120,28 @@ class Bubble { } } + /** + * @return the newer of {@link #getLastUpdateTime()} and {@link #getLastAccessTime()} + */ public long getLastActivity() { return Math.max(mLastUpdated, mLastAccessed); } /** + * @return the timestamp in milliseconds of the most recent notification entry for this bubble + */ + public long getLastUpdateTime() { + return mLastUpdated; + } + + /** + * @return the timestamp in milliseconds when this bubble was last displayed in expanded state + */ + public long getLastAccessTime() { + return mLastAccessed; + } + + /** * Should be invoked whenever a Bubble is accessed (selected while expanded). */ void markAsAccessedAt(long lastAccessedMillis) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index 7d189b28aa5e..48edf67a3ed4 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -29,6 +29,9 @@ import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.PARAMETER; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.Nullable; @@ -73,6 +76,7 @@ import com.android.systemui.statusbar.phone.StatusBarWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; import java.lang.annotation.Retention; +import java.lang.annotation.Target; import java.util.List; import javax.inject.Inject; @@ -88,11 +92,12 @@ import javax.inject.Singleton; public class BubbleController implements ConfigurationController.ConfigurationListener { private static final String TAG = "BubbleController"; - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; @Retention(SOURCE) @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE}) + @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} static final int DISMISS_USER_GESTURE = 1; @@ -103,7 +108,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi static final int DISMISS_ACCESSIBILITY_ACTION = 6; static final int DISMISS_NO_LONGER_BUBBLE = 7; - static final int MAX_BUBBLES = 5; // TODO: actually enforce this + public static final int MAX_BUBBLES = 5; // TODO: actually enforce this // Enables some subset of notifs to automatically become bubbles private static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false; @@ -510,6 +515,9 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Override public void onOrderChanged(List<Bubble> bubbles) { + if (mStackView != null) { + mStackView.updateBubbleOrder(bubbles); + } } @Override @@ -527,13 +535,6 @@ public class BubbleController implements ConfigurationController.ConfigurationLi } @Override - public void showFlyoutText(Bubble bubble, String text) { - if (mStackView != null) { - mStackView.animateInFlyoutForBubble(bubble); - } - } - - @Override public void apply() { mNotificationEntryManager.updateNotifications(); updateVisibility(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index f15ba6ee673b..9156e06fe54e 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -23,6 +23,7 @@ import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.util.Log; +import android.util.Pair; import androidx.annotation.Nullable; @@ -53,10 +54,10 @@ public class BubbleData { private static final int MAX_BUBBLES = 5; - private static final Comparator<Bubble> BUBBLES_BY_LAST_ACTIVITY_DESCENDING = - Comparator.comparing(Bubble::getLastActivity).reversed(); + private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING = + Comparator.comparing(BubbleData::sortKey).reversed(); - private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_LAST_ACTIVITY_DESCENDING = + private static final Comparator<Map.Entry<String, Long>> GROUPS_BY_MAX_SORT_KEY_DESCENDING = Comparator.<Map.Entry<String, Long>, Long>comparing(Map.Entry::getValue).reversed(); /** @@ -105,9 +106,6 @@ public class BubbleData { */ void onExpandedChanged(boolean expanded); - /** Flyout text should animate in, showing the given text. */ - void showFlyoutText(Bubble bubble, String text); - /** Commit any pending operations (since last call of apply()) */ void apply(); } @@ -121,15 +119,19 @@ public class BubbleData { private Bubble mSelectedBubble; private boolean mExpanded; - // TODO: ensure this is invalidated at the appropriate time - private int mSelectedBubbleExpandedPosition = -1; + // State tracked during an operation -- keeps track of what listener events to dispatch. + private boolean mExpandedChanged; + private boolean mOrderChanged; + private boolean mSelectionChanged; + private Bubble mUpdatedBubble; + private Bubble mAddedBubble; + private final List<Pair<Bubble, Integer>> mRemovedBubbles = new ArrayList<>(); private TimeSource mTimeSource = System::currentTimeMillis; @Nullable private Listener mListener; - @VisibleForTesting @Inject public BubbleData(Context context) { mContext = context; @@ -154,18 +156,19 @@ public class BubbleData { } public void setExpanded(boolean expanded) { - if (setExpandedInternal(expanded)) { - dispatchApply(); + if (DEBUG) { + Log.d(TAG, "setExpanded: " + expanded); } + setExpandedInternal(expanded); + dispatchPendingChanges(); } public void setSelectedBubble(Bubble bubble) { if (DEBUG) { Log.d(TAG, "setSelectedBubble: " + bubble); } - if (setSelectedBubbleInternal(bubble)) { - dispatchApply(); - } + setSelectedBubbleInternal(bubble); + dispatchPendingChanges(); } public void notificationEntryUpdated(NotificationEntry entry) { @@ -177,12 +180,12 @@ public class BubbleData { // Create a new bubble bubble = new Bubble(entry, this::onBubbleBlocked); doAdd(bubble); - dispatchOnBubbleAdded(bubble); + trim(); } else { // Updates an existing bubble bubble.setEntry(entry); doUpdate(bubble); - dispatchOnBubbleUpdated(bubble); + mUpdatedBubble = bubble; } if (shouldAutoExpand(entry)) { setSelectedBubbleInternal(bubble); @@ -192,7 +195,15 @@ public class BubbleData { } else if (mSelectedBubble == null) { setSelectedBubbleInternal(bubble); } - dispatchApply(); + dispatchPendingChanges(); + } + + public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) { + if (DEBUG) { + Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason); + } + doRemove(entry.key, reason); + dispatchPendingChanges(); } private void doAdd(Bubble bubble) { @@ -202,14 +213,21 @@ public class BubbleData { int minInsertPoint = 0; boolean newGroup = !hasBubbleWithGroupId(bubble.getGroupId()); if (isExpanded()) { - // first bubble of a group goes to the end, otherwise it goes within the existing group - minInsertPoint = - newGroup ? mBubbles.size() : findFirstIndexForGroup(bubble.getGroupId()); + // first bubble of a group goes to the beginning, otherwise within the existing group + minInsertPoint = newGroup ? 0 : findFirstIndexForGroup(bubble.getGroupId()); + } + if (insertBubble(minInsertPoint, bubble) < mBubbles.size() - 1) { + mOrderChanged = true; } - insertBubble(minInsertPoint, bubble); + mAddedBubble = bubble; if (!isExpanded()) { - packGroup(findFirstIndexForGroup(bubble.getGroupId())); + mOrderChanged |= packGroup(findFirstIndexForGroup(bubble.getGroupId())); + // Top bubble becomes selected. + setSelectedBubbleInternal(mBubbles.get(0)); } + } + + private void trim() { if (mBubbles.size() > MAX_BUBBLES) { mBubbles.stream() // sort oldest first (ascending lastActivity) @@ -217,10 +235,7 @@ public class BubbleData { // skip the selected bubble .filter((b) -> !b.equals(mSelectedBubble)) .findFirst() - .ifPresent((b) -> { - doRemove(b.getKey(), BubbleController.DISMISS_AGED); - dispatchApply(); - }); + .ifPresent((b) -> doRemove(b.getKey(), BubbleController.DISMISS_AGED)); } } @@ -229,43 +244,48 @@ public class BubbleData { Log.d(TAG, "doUpdate: " + bubble); } if (!isExpanded()) { - // while collapsed, update causes re-sort + // while collapsed, update causes re-pack + int prevPos = mBubbles.indexOf(bubble); mBubbles.remove(bubble); - insertBubble(0, bubble); - packGroup(findFirstIndexForGroup(bubble.getGroupId())); - } - } - - public void notificationEntryRemoved(NotificationEntry entry, @DismissReason int reason) { - if (DEBUG) { - Log.d(TAG, "notificationEntryRemoved: entry=" + entry + " reason=" + reason); + int newPos = insertBubble(0, bubble); + if (prevPos != newPos) { + packGroup(newPos); + mOrderChanged = true; + } + setSelectedBubbleInternal(mBubbles.get(0)); } - doRemove(entry.key, reason); - dispatchApply(); } private void doRemove(String key, @DismissReason int reason) { int indexToRemove = indexForKey(key); - if (indexToRemove >= 0) { - Bubble bubbleToRemove = mBubbles.get(indexToRemove); - if (mBubbles.size() == 1) { - // Going to become empty, handle specially. - setExpandedInternal(false); - setSelectedBubbleInternal(null); - } - mBubbles.remove(indexToRemove); - dispatchOnBubbleRemoved(bubbleToRemove, reason); - - // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. - if (Objects.equals(mSelectedBubble, bubbleToRemove)) { - // Move selection to the new bubble at the same position. - int newIndex = Math.min(indexToRemove, mBubbles.size() - 1); - Bubble newSelected = mBubbles.get(newIndex); - setSelectedBubbleInternal(newSelected); - } - bubbleToRemove.setDismissed(); - maybeSendDeleteIntent(reason, bubbleToRemove.entry); + if (indexToRemove == -1) { + return; + } + Bubble bubbleToRemove = mBubbles.get(indexToRemove); + if (mBubbles.size() == 1) { + // Going to become empty, handle specially. + setExpandedInternal(false); + setSelectedBubbleInternal(null); + } + if (indexToRemove < mBubbles.size() - 1) { + // Removing anything but the last bubble means positions will change. + mOrderChanged = true; + } + mBubbles.remove(indexToRemove); + mRemovedBubbles.add(Pair.create(bubbleToRemove, reason)); + if (!isExpanded()) { + mOrderChanged |= repackAll(); + } + + // Note: If mBubbles.isEmpty(), then mSelectedBubble is now null. + if (Objects.equals(mSelectedBubble, bubbleToRemove)) { + // Move selection to the new bubble at the same position. + int newIndex = Math.min(indexToRemove, mBubbles.size() - 1); + Bubble newSelected = mBubbles.get(newIndex); + setSelectedBubbleInternal(newSelected); } + bubbleToRemove.setDismissed(); + maybeSendDeleteIntent(reason, bubbleToRemove.entry); } public void dismissAll(@DismissReason int reason) { @@ -281,56 +301,71 @@ public class BubbleData { Bubble bubble = mBubbles.remove(0); bubble.setDismissed(); maybeSendDeleteIntent(reason, bubble.entry); - dispatchOnBubbleRemoved(bubble, reason); + mRemovedBubbles.add(Pair.create(bubble, reason)); } - dispatchApply(); + dispatchPendingChanges(); } - private void dispatchApply() { - if (mListener != null) { - mListener.apply(); + + private void dispatchPendingChanges() { + if (mListener == null) { + mExpandedChanged = false; + mAddedBubble = null; + mSelectionChanged = false; + mRemovedBubbles.clear(); + mUpdatedBubble = null; + mOrderChanged = false; + return; } - } + boolean anythingChanged = false; - private void dispatchOnBubbleAdded(Bubble bubble) { - if (mListener != null) { - mListener.onBubbleAdded(bubble); + if (mAddedBubble != null) { + mListener.onBubbleAdded(mAddedBubble); + mAddedBubble = null; + anythingChanged = true; } - } - private void dispatchOnBubbleRemoved(Bubble bubble, @DismissReason int reason) { - if (mListener != null) { - mListener.onBubbleRemoved(bubble, reason); + // Compat workaround: Always collapse first. + if (mExpandedChanged && !mExpanded) { + mListener.onExpandedChanged(mExpanded); + mExpandedChanged = false; + anythingChanged = true; } - } - private void dispatchOnExpandedChanged(boolean expanded) { - if (mListener != null) { - mListener.onExpandedChanged(expanded); + if (mSelectionChanged) { + mListener.onSelectionChanged(mSelectedBubble); + mSelectionChanged = false; + anythingChanged = true; } - } - private void dispatchOnSelectionChanged(@Nullable Bubble bubble) { - if (mListener != null) { - mListener.onSelectionChanged(bubble); + if (!mRemovedBubbles.isEmpty()) { + for (Pair<Bubble, Integer> removed : mRemovedBubbles) { + mListener.onBubbleRemoved(removed.first, removed.second); + } + mRemovedBubbles.clear(); + anythingChanged = true; } - } - private void dispatchOnBubbleUpdated(Bubble bubble) { - if (mListener != null) { - mListener.onBubbleUpdated(bubble); + if (mUpdatedBubble != null) { + mListener.onBubbleUpdated(mUpdatedBubble); + mUpdatedBubble = null; + anythingChanged = true; } - } - private void dispatchOnOrderChanged(List<Bubble> bubbles) { - if (mListener != null) { - mListener.onOrderChanged(bubbles); + if (mOrderChanged) { + mListener.onOrderChanged(mBubbles); + mOrderChanged = false; + anythingChanged = true; } - } - private void dispatchShowFlyoutText(Bubble bubble, String text) { - if (mListener != null) { - mListener.showFlyoutText(bubble, text); + if (mExpandedChanged) { + mListener.onExpandedChanged(mExpanded); + mExpandedChanged = false; + anythingChanged = true; + } + + if (anythingChanged) { + mListener.apply(); } } @@ -339,29 +374,25 @@ public class BubbleData { * the value changes. * * @param bubble the new selected bubble - * @return true if the state changed as a result */ - private boolean setSelectedBubbleInternal(@Nullable Bubble bubble) { + private void setSelectedBubbleInternal(@Nullable Bubble bubble) { if (DEBUG) { Log.d(TAG, "setSelectedBubbleInternal: " + bubble); } if (Objects.equals(bubble, mSelectedBubble)) { - return false; + return; } if (bubble != null && !mBubbles.contains(bubble)) { Log.e(TAG, "Cannot select bubble which doesn't exist!" + " (" + bubble + ") bubbles=" + mBubbles); - return false; + return; } if (mExpanded && bubble != null) { bubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); } mSelectedBubble = bubble; - dispatchOnSelectionChanged(mSelectedBubble); - if (!mExpanded || mSelectedBubble == null) { - mSelectedBubbleExpandedPosition = -1; - } - return true; + mSelectionChanged = true; + return; } /** @@ -369,37 +400,53 @@ public class BubbleData { * the value changes. * * @param shouldExpand the new requested state - * @return true if the state changed as a result */ - private boolean setExpandedInternal(boolean shouldExpand) { + private void setExpandedInternal(boolean shouldExpand) { if (DEBUG) { Log.d(TAG, "setExpandedInternal: shouldExpand=" + shouldExpand); } if (mExpanded == shouldExpand) { - return false; - } - if (mSelectedBubble != null) { - mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); + return; } if (shouldExpand) { if (mBubbles.isEmpty()) { Log.e(TAG, "Attempt to expand stack when empty!"); - return false; + return; } if (mSelectedBubble == null) { Log.e(TAG, "Attempt to expand stack without selected bubble!"); - return false; + return; + } + mSelectedBubble.markAsAccessedAt(mTimeSource.currentTimeMillis()); + mOrderChanged |= repackAll(); + } else if (!mBubbles.isEmpty()) { + // Apply ordering and grouping rules from expanded -> collapsed, then save + // the result. + mOrderChanged |= repackAll(); + // Save the state which should be returned to when expanded (with no other changes) + + if (mBubbles.indexOf(mSelectedBubble) > 0) { + // Move the selected bubble to the top while collapsed. + if (!mSelectedBubble.isOngoing() && mBubbles.get(0).isOngoing()) { + // The selected bubble cannot be raised to the first position because + // there is an ongoing bubble there. Instead, force the top ongoing bubble + // to become selected. + setSelectedBubbleInternal(mBubbles.get(0)); + } else { + // Raise the selected bubble (and it's group) up to the front so the selected + // bubble remains on top. + mBubbles.remove(mSelectedBubble); + mBubbles.add(0, mSelectedBubble); + packGroup(0); + } } - } else { - repackAll(); } mExpanded = shouldExpand; - dispatchOnExpandedChanged(mExpanded); - return true; + mExpandedChanged = true; } private static long sortKey(Bubble bubble) { - long key = bubble.getLastActivity(); + long key = bubble.getLastUpdateTime(); if (bubble.isOngoing()) { // Set 2nd highest bit (signed long int), to partition between ongoing and regular key |= 0x4000000000000000L; @@ -456,8 +503,9 @@ public class BubbleData { * unchanged. Relative order of any other bubbles are also unchanged. * * @param position the position of the first bubble for the group + * @return true if the position of any bubbles has changed as a result */ - private void packGroup(int position) { + private boolean packGroup(int position) { if (DEBUG) { Log.d(TAG, "packGroup: position=" + position); } @@ -471,16 +519,27 @@ public class BubbleData { moving.add(0, mBubbles.get(i)); } } + if (moving.isEmpty()) { + return false; + } mBubbles.removeAll(moving); mBubbles.addAll(position + 1, moving); + return true; } - private void repackAll() { + /** + * This applies a full sort and group pass to all existing bubbles. The bubbles are grouped + * by groupId. Each group is then sorted by the max(lastUpdated) time of it's bubbles. Bubbles + * within each group are then sorted by lastUpdated descending. + * + * @return true if the position of any bubbles changed as a result + */ + private boolean repackAll() { if (DEBUG) { Log.d(TAG, "repackAll()"); } if (mBubbles.isEmpty()) { - return; + return false; } Map<String, Long> groupLastActivity = new HashMap<>(); for (Bubble bubble : mBubbles) { @@ -494,7 +553,7 @@ public class BubbleData { // Sort groups by their most recently active bubble List<String> groupsByMostRecentActivity = groupLastActivity.entrySet().stream() - .sorted(GROUPS_BY_LAST_ACTIVITY_DESCENDING) + .sorted(GROUPS_BY_MAX_SORT_KEY_DESCENDING) .map(Map.Entry::getKey) .collect(toList()); @@ -504,10 +563,14 @@ public class BubbleData { for (String appId : groupsByMostRecentActivity) { mBubbles.stream() .filter((b) -> b.getGroupId().equals(appId)) - .sorted(BUBBLES_BY_LAST_ACTIVITY_DESCENDING) + .sorted(BUBBLES_BY_SORT_KEY_DESCENDING) .forEachOrdered(repacked::add); } + if (repacked.equals(mBubbles)) { + return false; + } mBubbles = repacked; + return true; } private void maybeSendDeleteIntent(@DismissReason int reason, NotificationEntry entry) { @@ -527,21 +590,25 @@ public class BubbleData { } private void onBubbleBlocked(NotificationEntry entry) { - boolean changed = false; - final String blockedPackage = entry.notification.getPackageName(); + final String blockedGroupId = Bubble.groupId(entry); + int selectedIndex = mBubbles.indexOf(mSelectedBubble); for (Iterator<Bubble> i = mBubbles.iterator(); i.hasNext(); ) { Bubble bubble = i.next(); - if (bubble.getPackageName().equals(blockedPackage)) { + if (bubble.getGroupId().equals(blockedGroupId)) { + mRemovedBubbles.add(Pair.create(bubble, BubbleController.DISMISS_BLOCKED)); i.remove(); - // TODO: handle removal of selected bubble, and collapse safely if emptied (see - // dismissAll) - dispatchOnBubbleRemoved(bubble, BubbleController.DISMISS_BLOCKED); - changed = true; } } - if (changed) { - dispatchApply(); - } + if (mBubbles.isEmpty()) { + setExpandedInternal(false); + setSelectedBubbleInternal(null); + } else if (!mBubbles.contains(mSelectedBubble)) { + // choose a new one + int newIndex = Math.min(selectedIndex, mBubbles.size() - 1); + Bubble newSelected = mBubbles.get(newIndex); + setSelectedBubbleInternal(newSelected); + } + dispatchPendingChanges(); } private int indexForKey(String key) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java new file mode 100644 index 000000000000..4db1e276f431 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDismissView.java @@ -0,0 +1,227 @@ +/* + * 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.bubbles; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; + +import com.android.systemui.R; + +/** Dismiss view that contains a scrim gradient, as well as a dismiss icon, text, and circle. */ +public class BubbleDismissView extends FrameLayout { + /** Duration for animations involving the dismiss target text/icon/gradient. */ + private static final int DISMISS_TARGET_ANIMATION_BASE_DURATION = 150; + + private View mDismissGradient; + + private LinearLayout mDismissTarget; + private ImageView mDismissIcon; + private TextView mDismissText; + private View mDismissCircle; + + private SpringAnimation mDismissTargetAlphaSpring; + private SpringAnimation mDismissTargetVerticalSpring; + + public BubbleDismissView(Context context) { + super(context); + setVisibility(GONE); + + mDismissGradient = new FrameLayout(mContext); + + FrameLayout.LayoutParams gradientParams = + new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); + gradientParams.gravity = Gravity.BOTTOM; + mDismissGradient.setLayoutParams(gradientParams); + + Drawable gradient = mContext.getResources().getDrawable(R.drawable.pip_dismiss_scrim); + gradient.setAlpha((int) (255 * 0.85f)); + mDismissGradient.setBackground(gradient); + + mDismissGradient.setVisibility(GONE); + addView(mDismissGradient); + + LayoutInflater.from(context).inflate(R.layout.bubble_dismiss_target, this, true); + mDismissTarget = findViewById(R.id.bubble_dismiss_icon_container); + mDismissIcon = findViewById(R.id.bubble_dismiss_close_icon); + mDismissText = findViewById(R.id.bubble_dismiss_text); + mDismissCircle = findViewById(R.id.bubble_dismiss_circle); + + // Set up the basic target area animations. These are very simple animations that don't need + // fancy interpolators. + final AccelerateDecelerateInterpolator interpolator = + new AccelerateDecelerateInterpolator(); + mDismissGradient.animate() + .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION) + .setInterpolator(interpolator); + mDismissText.animate() + .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION) + .setInterpolator(interpolator); + mDismissIcon.animate() + .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION) + .setInterpolator(interpolator); + mDismissCircle.animate() + .setDuration(DISMISS_TARGET_ANIMATION_BASE_DURATION / 2) + .setInterpolator(interpolator); + + mDismissTargetAlphaSpring = + new SpringAnimation(mDismissTarget, DynamicAnimation.ALPHA) + .setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_LOW) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); + mDismissTargetVerticalSpring = + new SpringAnimation(mDismissTarget, DynamicAnimation.TRANSLATION_Y) + .setSpring(new SpringForce() + .setStiffness(SpringForce.STIFFNESS_MEDIUM) + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)); + + mDismissTargetAlphaSpring.addEndListener((anim, canceled, alpha, velocity) -> { + // Since DynamicAnimations end when they're 'nearly' done, we can't rely on alpha being + // exactly zero when this listener is triggered. However, if it's less than 50% we can + // safely assume it was animating out rather than in. + if (alpha < 0.5f) { + // If the alpha spring was animating the view out, set it to GONE when it's done. + setVisibility(GONE); + } + }); + } + + /** Springs in the dismiss target and fades in the gradient. */ + void springIn() { + setVisibility(View.VISIBLE); + + // Fade in the dismiss target (icon + text). + mDismissTarget.setAlpha(0f); + mDismissTargetAlphaSpring.animateToFinalPosition(1f); + + // Spring up the dismiss target (icon + text). + mDismissTarget.setTranslationY(mDismissTarget.getHeight() / 2f); + mDismissTargetVerticalSpring.animateToFinalPosition(0); + + // Fade in the gradient. + mDismissGradient.setVisibility(VISIBLE); + mDismissGradient.animate().alpha(1f); + + // Make sure the dismiss elements are in the separated position (in case we hid the target + // while they were condensed to cover the bubbles being in the target). + mDismissIcon.setAlpha(1f); + mDismissIcon.setScaleX(1f); + mDismissIcon.setScaleY(1f); + mDismissIcon.setTranslationX(0f); + mDismissText.setAlpha(1f); + mDismissText.setTranslationX(0f); + } + + /** Springs out the dismiss target and fades out the gradient. */ + void springOut() { + // Fade out the target. + mDismissTargetAlphaSpring.animateToFinalPosition(0f); + + // Spring the target down a bit. + mDismissTargetVerticalSpring.animateToFinalPosition(mDismissTarget.getHeight() / 2f); + + // Fade out the gradient and then set it to GONE so it's not in the SBV hierarchy. + mDismissGradient.animate().alpha(0f).withEndAction( + () -> mDismissGradient.setVisibility(GONE)); + + // Pop out the dismiss circle. + mDismissCircle.animate().alpha(0f).scaleX(1.2f).scaleY(1.2f); + } + + /** + * Encircles the center of the dismiss target, pulling the X towards the center and hiding the + * text. + */ + void animateEncircleCenterWithX(boolean encircle) { + // Pull the text towards the center if we're encircling (it'll be faded out, leaving only + // the X icon over the bubbles), or back to normal if we're un-encircling. + final float textTranslation = encircle + ? -mDismissIcon.getWidth() / 4f + : 0f; + + // Center the icon if we're encircling, or put it back to normal if not. + final float iconTranslation = encircle + ? mDismissTarget.getWidth() / 2f + - mDismissIcon.getWidth() / 2f + - mDismissIcon.getLeft() + : 0f; + + // Fade in/out the text and translate it. + mDismissText.animate() + .alpha(encircle ? 0f : 1f) + .translationX(textTranslation); + + mDismissIcon.animate() + .setDuration(150) + .translationX(iconTranslation); + + // Fade out the gradient if we're encircling (the bubbles will 'absorb' it by darkening + // themselves). + mDismissGradient.animate() + .alpha(encircle ? 0f : 1f); + + // Prepare the circle to be 'dropped in'. + if (encircle) { + mDismissCircle.setAlpha(0f); + mDismissCircle.setScaleX(1.2f); + mDismissCircle.setScaleY(1.2f); + } + + // Drop in the circle, or pull it back up. + mDismissCircle.animate() + .alpha(encircle ? 1f : 0f) + .scaleX(encircle ? 1f : 0f) + .scaleY(encircle ? 1f : 0f); + } + + /** Animates the circle and the centered icon out. */ + void animateEncirclingCircleDisappearance() { + // Pop out the dismiss icon and circle. + mDismissIcon.animate() + .setDuration(50) + .scaleX(0.9f) + .scaleY(0.9f) + .alpha(0f); + mDismissCircle.animate() + .scaleX(0.9f) + .scaleY(0.9f) + .alpha(0f); + } + + /** Returns the Y value of the center of the dismiss target. */ + float getDismissTargetCenterY() { + return getTop() + mDismissTarget.getTop() + mDismissTarget.getHeight() / 2f; + } + + /** Returns the dismiss target, which contains the text/icon and any added padding. */ + View getDismissTarget() { + return mDismissTarget; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 123d73dc6432..2b1742592fba 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -19,12 +19,18 @@ package com.android.systemui.bubbles; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; import android.annotation.NonNull; import android.content.Context; 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; import android.graphics.Rect; @@ -32,6 +38,8 @@ import android.graphics.RectF; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.ShapeDrawable; import android.os.Bundle; +import android.os.VibrationEffect; +import android.os.Vibrator; import android.service.notification.StatusBarNotification; import android.util.Log; import android.util.StatsLog; @@ -84,6 +92,9 @@ public class BubbleStackView extends FrameLayout { /** Max width of the flyout, in terms of percent of the screen width. */ private static final float FLYOUT_MAX_WIDTH_PERCENT = .6f; + /** Percent to darken the bubbles when they're in the dismiss target. */ + private static final float DARKEN_PERCENT = 0.3f; + /** How long to wait, in milliseconds, before hiding the flyout. */ @VisibleForTesting static final int FLYOUT_HIDE_AFTER = 5000; @@ -131,6 +142,10 @@ public class BubbleStackView extends FrameLayout { private final SpringAnimation mExpandedViewYAnim; private final BubbleData mBubbleData; + private final Vibrator mVibrator; + private final ValueAnimator mDesaturateAndDarkenAnimator; + private final Paint mDesaturateAndDarkenPaint = new Paint(); + private PhysicsAnimationLayout mBubbleContainer; private StackAnimationController mStackAnimationController; private ExpandedAnimationController mExpandedAnimationController; @@ -183,6 +198,20 @@ public class BubbleStackView extends FrameLayout { private boolean mViewUpdatedRequested = false; private boolean mIsExpansionAnimating = false; + private boolean mShowingDismiss = false; + + /** + * Whether the user is currently dragging their finger within the dismiss target. In this state + * the stack will be magnetized to the center of the target, so we shouldn't move it until the + * touch exits the dismiss target area. + */ + private boolean mDraggingInDismissTarget = false; + + /** Whether the stack is magneting towards the dismiss target. */ + private boolean mAnimatingMagnet = false; + + /** The view to desaturate/darken when magneted to the dismiss target. */ + private View mDesaturateAndDarkenTargetView; private LayoutInflater mInflater; @@ -222,6 +251,8 @@ public class BubbleStackView extends FrameLayout { @NonNull private final SurfaceSynchronizer mSurfaceSynchronizer; + private BubbleDismissView mDismissContainer; + private Runnable mAfterMagnet; public BubbleStackView(Context context, BubbleData data, @Nullable SurfaceSynchronizer synchronizer) { @@ -253,6 +284,8 @@ public class BubbleStackView extends FrameLayout { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); wm.getDefaultDisplay().getSize(mDisplaySize); + mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + int padding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); @@ -286,6 +319,13 @@ public class BubbleStackView extends FrameLayout { addView(mFlyoutContainer); setupFlyout(); + mDismissContainer = new BubbleDismissView(mContext); + mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams( + MATCH_PARENT, + getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height), + Gravity.BOTTOM)); + addView(mDismissContainer); + mExpandedViewXAnim = new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X); mExpandedViewXAnim.setSpring( @@ -342,6 +382,29 @@ public class BubbleStackView extends FrameLayout { // This must be a separate OnDrawListener since it should be called for every draw. getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); + + final ColorMatrix animatedMatrix = new ColorMatrix(); + final ColorMatrix darkenMatrix = new ColorMatrix(); + + mDesaturateAndDarkenAnimator = ValueAnimator.ofFloat(1f, 0f); + mDesaturateAndDarkenAnimator.addUpdateListener(animation -> { + final float animatedValue = (float) animation.getAnimatedValue(); + animatedMatrix.setSaturation(animatedValue); + + final float animatedDarkenValue = (1f - animatedValue) * DARKEN_PERCENT; + darkenMatrix.setScale( + 1f - animatedDarkenValue /* red */, + 1f - animatedDarkenValue /* green */, + 1f - animatedDarkenValue /* blue */, + 1f /* alpha */); + + // Concat the matrices so that the animatedMatrix both desaturates and darkens. + animatedMatrix.postConcat(darkenMatrix); + + // Update the paint and apply it to the bubble container. + mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix)); + mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint); + }); } /** @@ -549,6 +612,7 @@ public class BubbleStackView extends FrameLayout { mBubbleContainer.addView(bubble.iconView, 0, new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); ViewClippingUtil.setClippingDeactivated(bubble.iconView, true, mClippingParameters); + animateInFlyoutForBubble(bubble); requestUpdate(); logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED); } @@ -570,10 +634,19 @@ public class BubbleStackView extends FrameLayout { // via BubbleData.Listener void updateBubble(Bubble bubble) { + animateInFlyoutForBubble(bubble); requestUpdate(); logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__UPDATED); } + public void updateBubbleOrder(List<Bubble> bubbles) { + for (int i = 0; i < bubbles.size(); i++) { + Bubble bubble = bubbles.get(i); + mBubbleContainer.moveViewTo(bubble.iconView, i); + } + } + + /** * Changes the currently selected bubble. If the stack is already expanded, the newly selected * bubble will be shown immediately. This does not change the expanded state or change the @@ -828,23 +901,22 @@ public class BubbleStackView extends FrameLayout { } mExpandedAnimationController.dragBubbleOut(bubble, x, y); + springInDismissTarget(); } /** Called when a drag operation on an individual bubble has finished. */ public void onBubbleDragFinish( - View bubble, float x, float y, float velX, float velY, boolean dismissed) { + View bubble, float x, float y, float velX, float velY) { if (DEBUG) { - Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble + ", dismissed=" + dismissed); + Log.d(TAG, "onBubbleDragFinish: bubble=" + bubble); } + if (!mIsExpanded || mIsExpansionAnimating) { return; } - if (dismissed) { - mExpandedAnimationController.prepareForDismissalWithVelocity(bubble, velX, velY); - } else { - mExpandedAnimationController.snapBubbleBack(bubble, velX, velY); - } + mExpandedAnimationController.snapBubbleBack(bubble, velX, velY); + springOutDismissTargetAndHideCircle(); } void onDragStart() { @@ -860,6 +932,7 @@ public class BubbleStackView extends FrameLayout { hideFlyoutImmediate(); mIsDragging = true; + mDraggingInDismissTarget = false; } void onDragged(float x, float y) { @@ -867,7 +940,8 @@ public class BubbleStackView extends FrameLayout { return; } - mStackAnimationController.moveFirstBubbleWithStackFollowing(x, y); + springInDismissTarget(); + mStackAnimationController.moveStackFromTouch(x, y); } void onDragFinish(float x, float y, float velX, float velY) { @@ -884,10 +958,171 @@ public class BubbleStackView extends FrameLayout { mStackAnimationController.flingStackThenSpringToEdge(x, velX, velY); logBubbleEvent(null /* no bubble associated with bubble stack move */, StatsLog.BUBBLE_UICHANGED__ACTION__STACK_MOVED); + + springOutDismissTargetAndHideCircle(); } - void onDragFinishAsDismiss() { - mIsDragging = false; + /** Prepares and starts the desaturate/darken animation on the bubble stack. */ + private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) { + mDesaturateAndDarkenTargetView = targetView; + + if (desaturateAndDarken) { + // Use the animated paint for the bubbles. + mDesaturateAndDarkenTargetView.setLayerType( + View.LAYER_TYPE_HARDWARE, mDesaturateAndDarkenPaint); + mDesaturateAndDarkenAnimator.removeAllListeners(); + mDesaturateAndDarkenAnimator.start(); + } else { + mDesaturateAndDarkenAnimator.removeAllListeners(); + mDesaturateAndDarkenAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + // Stop using the animated paint. + resetDesaturationAndDarken(); + } + }); + mDesaturateAndDarkenAnimator.reverse(); + } + } + + private void resetDesaturationAndDarken() { + mDesaturateAndDarkenAnimator.removeAllListeners(); + mDesaturateAndDarkenAnimator.cancel(); + mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null); + } + + /** + * Magnets the stack to the target, while also transforming the target to encircle the stack and + * desaturating/darkening the bubbles. + */ + void animateMagnetToDismissTarget( + View magnetView, boolean toTarget, float x, float y, float velX, float velY) { + mDraggingInDismissTarget = toTarget; + + if (toTarget) { + // The Y-value for the bubble stack to be positioned in the center of the dismiss target + final float destY = mDismissContainer.getDismissTargetCenterY() - mBubbleSize / 2f; + + mAnimatingMagnet = true; + + final Runnable afterMagnet = () -> { + mAnimatingMagnet = false; + if (mAfterMagnet != null) { + mAfterMagnet.run(); + } + }; + + if (magnetView == this) { + mStackAnimationController.magnetToDismiss(velX, velY, destY, afterMagnet); + animateDesaturateAndDarken(mBubbleContainer, true); + } else { + mExpandedAnimationController.magnetBubbleToDismiss( + magnetView, velX, velY, destY, afterMagnet); + + animateDesaturateAndDarken(magnetView, true); + } + + mDismissContainer.animateEncircleCenterWithX(true); + + } else { + mAnimatingMagnet = false; + + if (magnetView == this) { + mStackAnimationController.demagnetizeFromDismissToPoint(x, y, velX, velY); + animateDesaturateAndDarken(mBubbleContainer, false); + } else { + mExpandedAnimationController.demagnetizeBubbleTo(x, y, velX, velY); + animateDesaturateAndDarken(magnetView, false); + } + + mDismissContainer.animateEncircleCenterWithX(false); + } + + mVibrator.vibrate(VibrationEffect.get(toTarget + ? VibrationEffect.EFFECT_CLICK + : VibrationEffect.EFFECT_TICK)); + } + + /** + * Magnets the stack to the dismiss target if it's not already there. Then, dismiss the stack + * using the 'implode' animation and animate out the target. + */ + void magnetToStackIfNeededThenAnimateDismissal( + View touchedView, float velX, float velY, Runnable after) { + final Runnable animateDismissal = () -> { + mAfterMagnet = null; + + mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); + mDismissContainer.animateEncirclingCircleDisappearance(); + + // 'Implode' the stack and then hide the dismiss target. + if (touchedView == this) { + mStackAnimationController.implodeStack( + () -> { + mAnimatingMagnet = false; + mShowingDismiss = false; + mDraggingInDismissTarget = false; + after.run(); + resetDesaturationAndDarken(); + }); + } else { + mExpandedAnimationController.dismissDraggedOutBubble(() -> { + mAnimatingMagnet = false; + mShowingDismiss = false; + mDraggingInDismissTarget = false; + resetDesaturationAndDarken(); + after.run(); + }); + } + }; + + if (mAnimatingMagnet) { + // If the magnet animation is currently playing, dismiss the stack after it's done. This + // happens if the stack is flung towards the target. + mAfterMagnet = animateDismissal; + } else if (mDraggingInDismissTarget) { + // If we're in the dismiss target, but not animating, we already magneted - dismiss + // immediately. + animateDismissal.run(); + } else { + // Otherwise, we need to start the magnet animation and then dismiss afterward. + animateMagnetToDismissTarget(touchedView, true, -1 /* x */, -1 /* y */, velX, velY); + mAfterMagnet = animateDismissal; + } + } + + /** Animates in the dismiss target, including the gradient behind it. */ + private void springInDismissTarget() { + if (mShowingDismiss) { + return; + } + + mShowingDismiss = true; + + // Show the dismiss container and bring it to the front so the bubbles will go behind it. + mDismissContainer.springIn(); + mDismissContainer.bringToFront(); + mDismissContainer.setZ(Short.MAX_VALUE - 1); + } + + /** + * Animates the dismiss target out, as well as the circle that encircles the bubbles, if they + * were dragged into the target and encircled. + */ + private void springOutDismissTargetAndHideCircle() { + if (!mShowingDismiss) { + return; + } + + mDismissContainer.springOut(); + mShowingDismiss = false; + } + + + /** Whether the location of the given MotionEvent is within the dismiss target area. */ + public boolean isInDismissTarget(MotionEvent ev) { + return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY()); } /** @@ -1056,7 +1291,7 @@ public class BubbleStackView extends FrameLayout { private void setupFlyout() { // Retrieve the styled floating background color. TypedArray ta = mContext.obtainStyledAttributes( - new int[] {android.R.attr.colorBackgroundFloating}); + new int[]{android.R.attr.colorBackgroundFloating}); final int floatingBackgroundColor = ta.getColor(0, Color.WHITE); ta.recycle(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java index 82e6279772f4..f429c2c124b3 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java @@ -16,8 +16,6 @@ package com.android.systemui.bubbles; -import static com.android.systemui.pip.phone.PipDismissViewController.SHOW_TARGET_DELAY; - import android.content.Context; import android.graphics.PointF; import android.os.Handler; @@ -27,17 +25,35 @@ import android.view.View; import android.view.ViewConfiguration; import com.android.systemui.Dependency; -import com.android.systemui.pip.phone.PipDismissViewController; /** * Handles interpreting touches on a {@link BubbleStackView}. This includes expanding, collapsing, * dismissing, and flings. */ class BubbleTouchHandler implements View.OnTouchListener { - /** Velocity required to dismiss a bubble without dragging it into the dismiss target. */ - private static final float DISMISS_MIN_VELOCITY = 4000f; + /** Velocity required to dismiss the stack without dragging it into the dismiss target. */ + private static final float STACK_DISMISS_MIN_VELOCITY = 4000f; + + /** + * Velocity required to dismiss an individual bubble without dragging it into the dismiss + * target. + * + * This is higher than the stack dismiss velocity since unlike the stack, a downward fling could + * also be an attempted gesture to return the bubble to the row of expanded bubbles, which would + * usually be below the dragged bubble. By increasing the required velocity, it's less likely + * that the user is trying to drop it back into the row vs. fling it away. + */ + private static final float INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY = 6000f; private static final String TAG = "BubbleTouchHandler"; + /** + * When the stack is flung towards the bottom of the screen, it'll be dismissed if it's flung + * towards the center of the screen (where the dismiss target is). This value is the width of + * the target area to be considered 'towards the target'. For example 50% means that the stack + * needs to be flung towards the middle 50%, and the 25% on the left and right sides won't + * count. + */ + private static final float DISMISS_FLING_TARGET_WIDTH_PERCENT = 0.5f; private final PointF mTouchDown = new PointF(); private final PointF mViewPositionOnTouchDown = new PointF(); @@ -45,7 +61,6 @@ class BubbleTouchHandler implements View.OnTouchListener { private final BubbleData mBubbleData; private BubbleController mController = Dependency.get(BubbleController.class); - private PipDismissViewController mDismissViewController; private boolean mMovedEnough; private int mTouchSlopSquared; @@ -53,12 +68,6 @@ class BubbleTouchHandler implements View.OnTouchListener { private boolean mInDismissTarget; private Handler mHandler = new Handler(); - private Runnable mShowDismissAffordance = new Runnable() { - @Override - public void run() { - mDismissViewController.showDismissTarget(); - } - }; /** View that was initially touched, when we received the first ACTION_DOWN event. */ private View mTouchedView; @@ -67,7 +76,6 @@ class BubbleTouchHandler implements View.OnTouchListener { BubbleData bubbleData, Context context) { final int touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mTouchSlopSquared = touchSlop * touchSlop; - mDismissViewController = new PipDismissViewController(context); mBubbleData = bubbleData; mStack = stackView; } @@ -104,11 +112,6 @@ class BubbleTouchHandler implements View.OnTouchListener { mTouchDown.set(rawX, rawY); - if (!isFlyout) { - mDismissViewController.createDismissTarget(); - mHandler.postDelayed(mShowDismissAffordance, SHOW_TARGET_DELAY); - } - if (isStack) { mViewPositionOnTouchDown.set(mStack.getStackPosition()); mStack.onDragStart(); @@ -140,9 +143,18 @@ class BubbleTouchHandler implements View.OnTouchListener { } } - // TODO - when we're in the target stick to it / animate in some way? - mInDismissTarget = mDismissViewController.updateTarget( - isStack ? mStack.getBubbleAt(0) : mTouchedView); + final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event); + if (currentlyInDismissTarget != mInDismissTarget) { + mInDismissTarget = currentlyInDismissTarget; + + mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000); + final float velX = mVelocityTracker.getXVelocity(); + final float velY = mVelocityTracker.getYVelocity(); + + // If the touch event is within the dismiss target, magnet the stack to it. + mStack.animateMagnetToDismissTarget( + mTouchedView, mInDismissTarget, viewX, viewY, velX, velY); + } break; case MotionEvent.ACTION_CANCEL: @@ -151,28 +163,40 @@ class BubbleTouchHandler implements View.OnTouchListener { case MotionEvent.ACTION_UP: trackMovement(event); - if (mInDismissTarget && isStack) { - mController.dismissStack(BubbleController.DISMISS_USER_GESTURE); - mStack.onDragFinishAsDismiss(); + mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000); + final float velX = mVelocityTracker.getXVelocity(); + final float velY = mVelocityTracker.getYVelocity(); + + final boolean shouldDismiss = + isStack + ? mInDismissTarget + || isFastFlingTowardsDismissTarget(rawX, rawY, velX, velY) + : mInDismissTarget + || velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY; + + if (shouldDismiss) { + final String individualBubbleKey = + isStack ? null : ((BubbleView) mTouchedView).getKey(); + mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY, + () -> { + if (isStack) { + mController.dismissStack(BubbleController.DISMISS_USER_GESTURE); + } else { + mController.removeBubble( + individualBubbleKey, + BubbleController.DISMISS_USER_GESTURE); + } + }); } else if (isFlyout) { // TODO(b/129768381): Expand if tapped, dismiss if swiped away. if (!mBubbleData.isExpanded() && !mMovedEnough) { mBubbleData.setExpanded(true); } } else if (mMovedEnough) { - mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000); - final float velX = mVelocityTracker.getXVelocity(); - final float velY = mVelocityTracker.getYVelocity(); if (isStack) { mStack.onDragFinish(viewX, viewY, velX, velY); } else { - final boolean dismissed = mInDismissTarget || velY > DISMISS_MIN_VELOCITY; - mStack.onBubbleDragFinish( - mTouchedView, viewX, viewY, velX, velY, /* dismissed */ dismissed); - if (dismissed) { - mController.removeBubble(((BubbleView) mTouchedView).getKey(), - BubbleController.DISMISS_USER_GESTURE); - } + mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY); } } else if (mTouchedView == mStack.getExpandedBubbleView()) { mBubbleData.setExpanded(false); @@ -191,9 +215,38 @@ class BubbleTouchHandler implements View.OnTouchListener { return true; } + /** + * Whether the given touch data represents a powerful fling towards the bottom-center of the + * screen (the dismiss target). + */ + private boolean isFastFlingTowardsDismissTarget( + float rawX, float rawY, float velX, float velY) { + // Not a fling downward towards the target if velocity is zero or negative. + if (velY <= 0) { + return false; + } + + float bottomOfScreenInterceptX = rawX; + + // Only do math if the X velocity is non-zero, otherwise X won't change. + if (velX != 0) { + // Rise over run... + final float slope = velY / velX; + // ...y = mx + b, b = y / mx... + final float yIntercept = rawY - slope * rawX; + // ...calculate the x value when y = bottom of the screen. + bottomOfScreenInterceptX = (mStack.getHeight() - yIntercept) / slope; + } + + final float dismissTargetWidth = + mStack.getWidth() * DISMISS_FLING_TARGET_WIDTH_PERCENT; + return velY > STACK_DISMISS_MIN_VELOCITY + && bottomOfScreenInterceptX > dismissTargetWidth / 2f + && bottomOfScreenInterceptX < mStack.getWidth() - dismissTargetWidth / 2f; + } + /** Clears all touch-related state. */ private void resetForNextGesture() { - cleanUpDismissTarget(); if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; @@ -203,15 +256,6 @@ class BubbleTouchHandler implements View.OnTouchListener { mInDismissTarget = false; } - /** - * Removes the dismiss target and cancels any pending callbacks to show it. - */ - private void cleanUpDismissTarget() { - mHandler.removeCallbacks(mShowDismissAffordance); - mDismissViewController.destroyDismissTarget(); - } - - private void trackMovement(MotionEvent event) { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java index 95fbfe33ee71..a9ad464867a2 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java @@ -64,6 +64,20 @@ public class ExpandedAnimationController /** Size of dismiss target at bottom of screen. */ private float mPipDismissHeight; + /** Whether the dragged-out bubble is in the dismiss target. */ + private boolean mIndividualBubbleWithinDismissTarget = false; + + /** + * Whether the dragged out bubble is springing towards the touch point, rather than using the + * default behavior of moving directly to the touch point. + * + * This happens when the user's finger exits the dismiss area while the bubble is magnetized to + * the center. Since the touch point differs from the bubble location, we need to animate the + * bubble back to the touch point to avoid a jarring instant location change from the center of + * the target to the touch point just outside the target bounds. + */ + private boolean mSpringingBubbleToTouch = false; + public ExpandedAnimationController(Point displaySize) { mDisplaySize = displaySize; } @@ -151,8 +165,23 @@ public class ExpandedAnimationController * bubble is dragged back into the row. */ public void dragBubbleOut(View bubbleView, float x, float y) { - bubbleView.setTranslationX(x); - bubbleView.setTranslationY(y); + if (mSpringingBubbleToTouch) { + if (mLayout.arePropertiesAnimatingOnView( + bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) { + animationForChild(mBubbleDraggingOut) + .translationX(x) + .translationY(y) + .withStiffness(SpringForce.STIFFNESS_HIGH) + .start(); + } else { + mSpringingBubbleToTouch = false; + } + } + + if (!mSpringingBubbleToTouch && !mIndividualBubbleWithinDismissTarget) { + bubbleView.setTranslationX(x); + bubbleView.setTranslationY(y); + } final boolean draggedOutEnough = y > getExpandedY() + mBubbleSizePx || y < getExpandedY() - mBubbleSizePx; @@ -164,6 +193,53 @@ public class ExpandedAnimationController } } + /** Plays a dismiss animation on the dragged out bubble. */ + public void dismissDraggedOutBubble(Runnable after) { + mIndividualBubbleWithinDismissTarget = false; + + // Fill the space from the soon to be dismissed bubble. + animateStackByBubbleWidthsStartingFrom( + /* numBubbleWidths */ -1, + /* startIndex */ mLayout.indexOfChild(mBubbleDraggingOut) + 1); + + animationForChild(mBubbleDraggingOut) + .withStiffness(SpringForce.STIFFNESS_HIGH) + .scaleX(1.1f) + .scaleY(1.1f) + .alpha(0f, after) + .start(); + } + + /** Magnets the given bubble to the dismiss target. */ + public void magnetBubbleToDismiss( + View bubbleView, float velX, float velY, float destY, Runnable after) { + mIndividualBubbleWithinDismissTarget = true; + mSpringingBubbleToTouch = false; + animationForChild(bubbleView) + .withStiffness(SpringForce.STIFFNESS_MEDIUM) + .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) + .withPositionStartVelocities(velX, velY) + .translationX(mLayout.getWidth() / 2f - mBubbleSizePx / 2f) + .translationY(destY, after) + .start(); + } + + /** + * Springs the dragged-out bubble towards the given coordinates and sets flags to have touch + * events update the spring's final position until it's settled. + */ + public void demagnetizeBubbleTo(float x, float y, float velX, float velY) { + mIndividualBubbleWithinDismissTarget = false; + mSpringingBubbleToTouch = true; + + animationForChild(mBubbleDraggingOut) + .translationX(x) + .translationY(y) + .withPositionStartVelocities(velX, velY) + .withStiffness(SpringForce.STIFFNESS_HIGH) + .start(); + } + /** * Snaps a bubble back to its position within the bubble row, and animates the rest of the * bubbles to accommodate it if it was previously dragged out past the threshold. @@ -274,28 +350,21 @@ public class ExpandedAnimationController @Override void onChildRemoved(View child, int index, Runnable finishRemoval) { - // Bubble pops out to the top. - // TODO: Reverse this when bubbles are at the bottom. - final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child); - animator.alpha(0f, finishRemoval /* endAction */); // If we're removing the dragged-out bubble, that means it got dismissed. if (child.equals(mBubbleDraggingOut)) { - animator.position( - mLayout.getWidth() / 2f - mBubbleSizePx / 2f, - mLayout.getHeight() + mBubbleSizePx) - .withPositionStartVelocities(mBubbleDraggingOutVelX, mBubbleDraggingOutVelY) - .scaleX(ANIMATE_SCALE_PERCENT) - .scaleY(ANIMATE_SCALE_PERCENT); - mBubbleDraggingOut = null; + finishRemoval.run(); } else { - animator.translationY(getExpandedY() - mBubbleSizePx * ANIMATE_TRANSLATION_FACTOR); + animator.alpha(0f, finishRemoval /* endAction */) + .withStiffness(SpringForce.STIFFNESS_HIGH) + .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) + .scaleX(1.1f) + .scaleY(1.1f) + .start(); } - animator.start(); - // Animate all the other bubbles to their new positions sans this bubble. animateBubblesAfterIndexToCorrectX(index); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java index 460652612593..997d2c4627d8 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayout.java @@ -290,6 +290,10 @@ public class PhysicsAnimationLayout extends FrameLayout { final Runnable checkIfAllFinished = () -> { if (!arePropertiesAnimating(properties)) { action.run(); + + for (DynamicAnimation.ViewProperty property : properties) { + removeEndActionForProperty(property); + } } }; @@ -379,10 +383,21 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Checks whether any animations of the given properties are still running. */ public boolean arePropertiesAnimating(DynamicAnimation.ViewProperty... properties) { for (int i = 0; i < getChildCount(); i++) { - for (DynamicAnimation.ViewProperty property : properties) { - if (getAnimationAtIndex(property, i).isRunning()) { - return true; - } + if (arePropertiesAnimatingOnView(getChildAt(i), properties)) { + return true; + } + } + + return false; + } + + /** Checks whether any animations of the given properties are running on the given view. */ + public boolean arePropertiesAnimatingOnView( + View view, DynamicAnimation.ViewProperty... properties) { + for (DynamicAnimation.ViewProperty property : properties) { + final SpringAnimation animation = getAnimationFromView(property, view); + if (animation != null && animation.isRunning()) { + return true; } } @@ -556,7 +571,11 @@ public class PhysicsAnimationLayout extends FrameLayout { DynamicAnimation anim, boolean canceled, float value, float velocity) { if (!arePropertiesAnimating(mProperty)) { if (mEndActionForProperty.containsKey(mProperty)) { - mEndActionForProperty.get(mProperty).run(); + final Runnable callback = mEndActionForProperty.get(mProperty); + + if (callback != null) { + callback.run(); + } } } } @@ -578,6 +597,12 @@ public class PhysicsAnimationLayout extends FrameLayout { /** Start delay to use when start is called. */ private long mStartDelay = 0; + /** Damping ratio to use for the animations. */ + private float mDampingRatio = -1; + + /** Stiffness to use for the animations. */ + private float mStiffness = -1; + /** End actions to call when animations for the given property complete. */ private Map<DynamicAnimation.ViewProperty, Runnable[]> mEndActionsForProperty = new HashMap<>(); @@ -687,6 +712,24 @@ public class PhysicsAnimationLayout extends FrameLayout { } /** + * Set the damping ratio to use for this animation. If not supplied, will default to the + * value from {@link PhysicsAnimationController#getSpringForce}. + */ + public PhysicsPropertyAnimator withDampingRatio(float dampingRatio) { + mDampingRatio = dampingRatio; + return this; + } + + /** + * Set the stiffness to use for this animation. If not supplied, will default to the + * value from {@link PhysicsAnimationController#getSpringForce}. + */ + public PhysicsPropertyAnimator withStiffness(float stiffness) { + mStiffness = stiffness; + return this; + } + + /** * Set the start velocities to use for TRANSLATION_X and TRANSLATION_Y animations. This * overrides any value set via {@link #withStartVelocity(float)} for those properties. */ @@ -711,12 +754,14 @@ public class PhysicsAnimationLayout extends FrameLayout { // If there are end actions, set an end listener on the layout for all the properties // we're about to animate. - if (after != null) { + if (after != null && after.length > 0) { final DynamicAnimation.ViewProperty[] propertiesArray = properties.toArray(new DynamicAnimation.ViewProperty[0]); - for (Runnable callback : after) { - setEndActionForMultipleProperties(callback, propertiesArray); - } + setEndActionForMultipleProperties(() -> { + for (Runnable callback : after) { + callback.run(); + } + }, propertiesArray); } // If we used position-specific end actions, we'll need to listen for both TRANSLATION_X @@ -746,12 +791,15 @@ public class PhysicsAnimationLayout extends FrameLayout { // Actually start the animations. for (DynamicAnimation.ViewProperty property : properties) { + final SpringForce defaultSpringForce = mController.getSpringForce(property, mView); animateValueForChild( property, mView, mAnimatedProperties.get(property), mPositionStartVelocities.getOrDefault(property, mDefaultStartVelocity), mStartDelay, + mStiffness >= 0 ? mStiffness : defaultSpringForce.getStiffness(), + mDampingRatio >= 0 ? mDampingRatio : defaultSpringForce.getDampingRatio(), mEndActionsForProperty.get(property)); } @@ -760,6 +808,8 @@ public class PhysicsAnimationLayout extends FrameLayout { mPositionStartVelocities.clear(); mDefaultStartVelocity = 0; mStartDelay = 0; + mStiffness = -1; + mDampingRatio = -1; mEndActionsForProperty.clear(); } @@ -778,6 +828,8 @@ public class PhysicsAnimationLayout extends FrameLayout { float value, float startVel, long startDelay, + float stiffness, + float dampingRatio, Runnable[] afterCallbacks) { if (view != null) { final SpringAnimation animation = @@ -795,6 +847,9 @@ public class PhysicsAnimationLayout extends FrameLayout { }); } + animation.getSpring().setStiffness(stiffness); + animation.getSpring().setDampingRatio(dampingRatio); + if (startVel > 0) { animation.setStartVelocity(startVel); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java index bc249aedc605..f937525cf417 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java @@ -30,7 +30,6 @@ import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController; import com.google.android.collect.Sets; @@ -116,6 +115,25 @@ public class StackAnimationController extends */ private boolean mIsMovingFromFlinging = false; + /** + * Whether the stack is within the dismiss target (either by being dragged, magnet'd, or flung). + */ + private boolean mWithinDismissTarget = false; + + /** + * Whether the first bubble is springing towards the touch point, rather than using the default + * behavior of moving directly to the touch point with the rest of the stack following it. + * + * This happens when the user's finger exits the dismiss area while the stack is magnetized to + * the center. Since the touch point differs from the stack location, we need to animate the + * stack back to the touch point to avoid a jarring instant location change from the center of + * the target to the touch point just outside the target bounds. + * + * This is reset once the spring animations end, since that means the first bubble has + * successfully 'caught up' to the touch. + */ + private boolean mFirstBubbleSpringingToTouch = false; + /** Horizontal offset of bubbles in the stack. */ private float mStackOffset; /** Diameter of the bubbles themselves. */ @@ -445,6 +463,120 @@ public class StackAnimationController extends return allowableRegion; } + /** Moves the stack in response to a touch event. */ + public void moveStackFromTouch(float x, float y) { + + // If we're springing to the touch point to 'catch up' after dragging out of the dismiss + // target, then update the stack position animations instead of moving the bubble directly. + if (mFirstBubbleSpringingToTouch) { + final SpringAnimation springToTouchX = + (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X); + final SpringAnimation springToTouchY = + (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y); + + // If either animation is still running, we haven't caught up. Update the animations. + if (springToTouchX.isRunning() || springToTouchY.isRunning()) { + springToTouchX.animateToFinalPosition(x); + springToTouchY.animateToFinalPosition(y); + } else { + // If the animations have finished, the stack is now at the touch point. We can + // resume moving the bubble directly. + mFirstBubbleSpringingToTouch = false; + } + } + + if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) { + moveFirstBubbleWithStackFollowing(x, y); + } + } + + /** + * Demagnetizes the stack, springing it towards the given point. This also sets flags so that + * subsequent touch events will update the final position of the demagnetization spring instead + * of directly moving the bubbles, until demagnetization is complete. + */ + public void demagnetizeFromDismissToPoint(float x, float y, float velX, float velY) { + mWithinDismissTarget = false; + mFirstBubbleSpringingToTouch = true; + + springFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_X, + new SpringForce() + .setDampingRatio(DEFAULT_BOUNCINESS) + .setStiffness(DEFAULT_STIFFNESS), + velX, x); + + springFirstBubbleWithStackFollowing( + DynamicAnimation.TRANSLATION_Y, + new SpringForce() + .setDampingRatio(DEFAULT_BOUNCINESS) + .setStiffness(DEFAULT_STIFFNESS), + velY, y); + } + + /** + * Spring the stack towards the dismiss target, respecting existing velocity. This also sets + * flags so that subsequent touch events will not move the stack until it's demagnetized. + */ + public void magnetToDismiss(float velX, float velY, float destY, Runnable after) { + mWithinDismissTarget = true; + mFirstBubbleSpringingToTouch = false; + + animationForChildAtIndex(0) + .translationX(mLayout.getWidth() / 2f - mIndividualBubbleSize / 2f) + .translationY(destY, after) + .withPositionStartVelocities(velX, velY) + .withStiffness(SpringForce.STIFFNESS_MEDIUM) + .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) + .start(); + } + + /** + * 'Implode' the stack by shrinking the bubbles via chained animations and fading them out. + */ + public void implodeStack(Runnable after) { + // Pop and fade the bubbles sequentially. + animationForChildAtIndex(0) + .scaleX(0.5f) + .scaleY(0.5f) + .alpha(0f) + .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY) + .withStiffness(SpringForce.STIFFNESS_HIGH) + .start(() -> { + // Run the callback and reset flags. The child translation animations might + // still be running, but that's fine. Once the alpha is at 0f they're no longer + // visible anyway. + after.run(); + mWithinDismissTarget = false; + }); + } + + /** + * Springs the first bubble to the given final position, with the rest of the stack 'following'. + */ + protected void springFirstBubbleWithStackFollowing( + DynamicAnimation.ViewProperty property, SpringForce spring, + float vel, float finalPosition) { + + if (mLayout.getChildCount() == 0) { + return; + } + + Log.d(TAG, String.format("Springing %s to final position %f.", + PhysicsAnimationLayout.getReadablePropertyName(property), + finalPosition)); + + StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); + SpringAnimation springAnimation = + new SpringAnimation(this, firstBubbleProperty) + .setSpring(spring) + .setStartVelocity(vel); + + cancelStackPositionAnimation(property); + mStackPositionAnimations.put(property, springAnimation); + springAnimation.animateToFinalPosition(finalPosition); + } + @Override Set<DynamicAnimation.ViewProperty> getAnimatedProperties() { return Sets.newHashSet( @@ -459,7 +591,9 @@ public class StackAnimationController extends int getNextAnimationInChain(DynamicAnimation.ViewProperty property, int index) { if (property.equals(DynamicAnimation.TRANSLATION_X) || property.equals(DynamicAnimation.TRANSLATION_Y)) { - return index + 1; // Just chain them linearly. + return index + 1; + } else if (mWithinDismissTarget) { + return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used). } else { return NONE; } @@ -469,9 +603,15 @@ public class StackAnimationController extends @Override float getOffsetForChainedPropertyAnimation(DynamicAnimation.ViewProperty property) { if (property.equals(DynamicAnimation.TRANSLATION_X)) { - // Offset to the left if we're on the left, or the right otherwise. - return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x) - ? -mStackOffset : mStackOffset; + // If we're in the dismiss target, have the bubbles pile on top of each other with no + // offset. + if (mWithinDismissTarget) { + return 0f; + } else { + // Offset to the left if we're on the left, or the right otherwise. + return mLayout.isFirstChildXLeftOfCenter(mStackPosition.x) + ? -mStackOffset : mStackOffset; + } } else { return 0f; } @@ -480,11 +620,8 @@ public class StackAnimationController extends @Override SpringForce getSpringForce(DynamicAnimation.ViewProperty property, View view) { return new SpringForce() - .setDampingRatio(BubbleController.getBubbleBounciness( - mLayout.getContext(), DEFAULT_BOUNCINESS)) - .setStiffness(BubbleController.getBubbleStiffness( - mLayout.getContext(), - mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS)); + .setDampingRatio(DEFAULT_BOUNCINESS) + .setStiffness(mIsMovingFromFlinging ? FLING_FOLLOW_STIFFNESS : DEFAULT_STIFFNESS); } @Override @@ -594,32 +731,6 @@ public class StackAnimationController extends } /** - * Springs the first bubble to the given final position, with the rest of the stack 'following'. - */ - private void springFirstBubbleWithStackFollowing( - DynamicAnimation.ViewProperty property, SpringForce spring, - float vel, float finalPosition) { - - if (mLayout.getChildCount() == 0) { - return; - } - - Log.d(TAG, String.format("Springing %s to final position %f.", - PhysicsAnimationLayout.getReadablePropertyName(property), - finalPosition)); - - StackPositionProperty firstBubbleProperty = new StackPositionProperty(property); - SpringAnimation springAnimation = - new SpringAnimation(this, firstBubbleProperty) - .setSpring(spring) - .setStartVelocity(vel); - - cancelStackPositionAnimation(property); - mStackPositionAnimations.put(property, springAnimation); - springAnimation.animateToFinalPosition(finalPosition); - } - - /** * Cancels any outstanding first bubble property animations that are running. This does not * affect the SpringAnimations controlling the individual bubbles' 'following' effect - it only * cancels animations started from {@link #springFirstBubbleWithStackFollowing} and diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java index 9052093346cb..a4bd24416f61 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java @@ -22,6 +22,7 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; +import android.hardware.biometrics.BiometricSourceType; import android.net.Uri; import android.os.Handler; import android.os.Looper; @@ -33,6 +34,8 @@ import android.view.MotionEvent; import android.view.accessibility.AccessibilityManager; import com.android.internal.logging.MetricsLogger; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.Dependency; import com.android.systemui.UiOffloadThread; import com.android.systemui.analytics.DataCollector; @@ -80,6 +83,7 @@ public class FalsingManagerImpl implements FalsingManagerFactory.FalsingManager private boolean mBouncerOffOnDown = false; private boolean mSessionActive = false; private boolean mIsTouchScreen = true; + private boolean mJustUnlockedWithFace = false; private int mState = StatusBarState.SHADE; private boolean mScreenOn; private boolean mShowingAod; @@ -120,6 +124,17 @@ public class FalsingManagerImpl implements FalsingManagerFactory.FalsingManager updateConfiguration(); } }; + private final KeyguardUpdateMonitorCallback mKeyguardUpdateCallback = + new KeyguardUpdateMonitorCallback() { + @Override + public void onBiometricAuthenticated(int userId, + BiometricSourceType biometricSourceType) { + if (userId == KeyguardUpdateMonitor.getCurrentUser() + && biometricSourceType == BiometricSourceType.FACE) { + mJustUnlockedWithFace = true; + } + } + }; FalsingManagerImpl(Context context) { mContext = context; @@ -138,6 +153,7 @@ public class FalsingManagerImpl implements FalsingManagerFactory.FalsingManager updateConfiguration(); Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener); + KeyguardUpdateMonitor.getInstance(context).registerCallback(mKeyguardUpdateCallback); } private void updateConfiguration() { @@ -199,6 +215,7 @@ public class FalsingManagerImpl implements FalsingManagerFactory.FalsingManager } mBouncerOn = false; mSessionActive = true; + mJustUnlockedWithFace = false; mIsFalseTouchCalls = 0; if (mHumanInteractionClassifier.isEnabled()) { @@ -285,6 +302,11 @@ public class FalsingManagerImpl implements FalsingManagerFactory.FalsingManager // anti-falsed. return false; } + if (mJustUnlockedWithFace) { + // Unlocking with face is a strong user presence signal, we can assume the user + // is present until the next session starts. + return false; + } mIsFalseTouchCalls++; boolean isFalse = mHumanInteractionClassifier.isFalseTouch(); if (!isFalse) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index acacc8fbb917..033c4fbed6f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -40,6 +40,7 @@ import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.util.FloatProperty; import android.util.Log; import android.util.Property; @@ -73,7 +74,10 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi * want to scale them (in a way that doesn't require an asset dump) down 2dp. So * 17dp * (15 / 17) = 15dp, the new height. */ - private static final float SYSTEM_ICON_SCALE = 15.f / 17.f; + private static final float SYSTEM_ICON_DESIRED_HEIGHT = 15f; + private static final float SYSTEM_ICON_INTRINSIC_HEIGHT = 17f; + private static final float SYSTEM_ICON_SCALE = + SYSTEM_ICON_DESIRED_HEIGHT / SYSTEM_ICON_INTRINSIC_HEIGHT; private final int ANIMATION_DURATION_FAST = 100; public static final int STATE_ICON = 0; @@ -202,8 +206,25 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi updatePivot(); } + // Makes sure that all icons are scaled to the same height (15dp). If we cannot get a height + // for the icon, it uses the default SCALE (15f / 17f) which is the old behavior private void updateIconScaleForSystemIcons() { - mIconScale = SYSTEM_ICON_SCALE; + float iconHeight = getIconHeightInDps(); + if (iconHeight != 0) { + mIconScale = SYSTEM_ICON_DESIRED_HEIGHT / iconHeight; + } else { + mIconScale = SYSTEM_ICON_SCALE; + } + } + + private float getIconHeightInDps() { + Drawable d = getDrawable(); + if (d != null) { + return ((float) getDrawable().getIntrinsicHeight() * DisplayMetrics.DENSITY_DEFAULT) + / mDensity; + } else { + return SYSTEM_ICON_INTRINSIC_HEIGHT; + } } public float getIconScaleFullyDark() { @@ -221,8 +242,8 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi if (density != mDensity) { mDensity = density; reloadDimens(); - maybeUpdateIconScaleDimens(); updateDrawable(); + maybeUpdateIconScaleDimens(); } boolean nightMode = (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; @@ -305,6 +326,8 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi if (!updateDrawable(false /* no clear */)) return false; // we have to clear the grayscale tag since it may have changed setTag(R.id.icon_is_grayscale, null); + // Maybe set scale based on icon height + maybeUpdateIconScaleDimens(); } if (!levelEquals) { setImageLevel(icon.iconLevel); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 27368deac847..50e406f5936e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -86,7 +86,7 @@ public class PhoneStatusBarPolicy private static final String TAG = "PhoneStatusBarPolicy"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - public static final int LOCATION_STATUS_ICON_ID = R.drawable.stat_sys_location; + public static final int LOCATION_STATUS_ICON_ID = PrivacyType.TYPE_LOCATION.getIconId(); private final String mSlotCast; private final String mSlotHotspot; @@ -230,10 +230,10 @@ public class PhoneStatusBarPolicy mIconController.setIconVisibility(mSlotDataSaver, false); // privacy items - mIconController.setIcon(mSlotMicrophone, R.drawable.stat_sys_mic_none, + mIconController.setIcon(mSlotMicrophone, PrivacyType.TYPE_MICROPHONE.getIconId(), PrivacyType.TYPE_MICROPHONE.getName(mContext)); mIconController.setIconVisibility(mSlotMicrophone, false); - mIconController.setIcon(mSlotCamera, R.drawable.stat_sys_camera, + mIconController.setIcon(mSlotCamera, PrivacyType.TYPE_CAMERA.getIconId(), PrivacyType.TYPE_CAMERA.getName(mContext)); mIconController.setIconVisibility(mSlotCamera, false); mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID, 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 2d697e34c626..35a15167d207 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -296,22 +296,22 @@ public class BubbleControllerTest extends SysuiTestCase { BubbleStackView stackView = mBubbleController.getStackView(); mBubbleController.expandStack(); assertTrue(mBubbleController.isStackExpanded()); - verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); - - // First added is the one that is expanded - assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry()); - assertFalse(mRow.getEntry().showInShadeWhenBubble()); + verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().key); - // Switch which bubble is expanded - mBubbleController.selectBubble(mRow2.getEntry().key); - stackView.setExpandedBubble(mRow2.getEntry()); + // Last added is the one that is expanded assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry()); assertFalse(mRow2.getEntry().showInShadeWhenBubble()); + // Switch which bubble is expanded + mBubbleController.selectBubble(mRow.getEntry().key); + stackView.setExpandedBubble(mRow.getEntry()); + assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry()); + assertFalse(mRow.getEntry().showInShadeWhenBubble()); + // collapse for previous bubble - verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().key); // expand for selected bubble - verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); // Collapse mBubbleController.collapseStack(); @@ -352,27 +352,27 @@ public class BubbleControllerTest extends SysuiTestCase { mBubbleController.expandStack(); assertTrue(mBubbleController.isStackExpanded()); - verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().key); - // First added is the one that is expanded - assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry()); - assertFalse(mRow.getEntry().showInShadeWhenBubble()); + // Last added is the one that is expanded + assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry()); + assertFalse(mRow2.getEntry().showInShadeWhenBubble()); // Dismiss currently expanded mBubbleController.removeBubble(stackView.getExpandedBubbleView().getKey(), BubbleController.DISMISS_USER_GESTURE); - verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().key); - // Make sure next bubble is selected - assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry()); - verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().key); + // Make sure first bubble is selected + assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry()); + verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().key); // Dismiss that one mBubbleController.removeBubble(stackView.getExpandedBubbleView().getKey(), BubbleController.DISMISS_USER_GESTURE); // Make sure state changes and collapse happens - verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().key); + verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow.getEntry().key); verify(mBubbleStateChangeListener).onHasBubblesChanged(false); assertFalse(mBubbleController.hasBubbles()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java index d6dac2f36ba1..33b2e6e59470 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java @@ -16,12 +16,18 @@ package com.android.systemui.bubbles; +import static com.android.systemui.bubbles.BubbleController.DISMISS_AGED; + import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -48,6 +54,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -108,61 +116,11 @@ public class BubbleDataTest extends SysuiTestCase { // Used by BubbleData to set lastAccessedTime when(mTimeSource.currentTimeMillis()).thenReturn(1000L); mBubbleData.setTimeSource(mTimeSource); - } - - private NotificationEntry createBubbleEntry(int userId, String notifKey, String packageName) { - return createBubbleEntry(userId, notifKey, packageName, 1000); - } - - private void setPostTime(NotificationEntry entry, long postTime) { - when(entry.notification.getPostTime()).thenReturn(postTime); - } - - private void setOngoing(NotificationEntry entry, boolean ongoing) { - if (ongoing) { - entry.notification.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; - } else { - entry.notification.getNotification().flags &= ~Notification.FLAG_FOREGROUND_SERVICE; - } - } - /** - * No ExpandableNotificationRow is required to test BubbleData. This setup is all that is - * required for BubbleData functionality and verification. NotificationTestHelper is used only - * as a convenience to create a Notification w/BubbleMetadata. - */ - private NotificationEntry createBubbleEntry(int userId, String notifKey, String packageName, - long postTime) { - // BubbleMetadata - Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder() - .setIntent(mExpandIntent) - .setDeleteIntent(mDeleteIntent) - .setIcon(Icon.createWithResource("", 0)) - .build(); - // Notification -> BubbleMetadata - Notification notification = mNotificationTestHelper.createNotification(false, - null /* groupKey */, bubbleMetadata); - - // StatusBarNotification - StatusBarNotification sbn = mock(StatusBarNotification.class); - when(sbn.getKey()).thenReturn(notifKey); - when(sbn.getUser()).thenReturn(new UserHandle(userId)); - when(sbn.getPackageName()).thenReturn(packageName); - when(sbn.getPostTime()).thenReturn(postTime); - when(sbn.getNotification()).thenReturn(notification); - - // NotificationEntry -> StatusBarNotification -> Notification -> BubbleMetadata - return new NotificationEntry(sbn); - } - - private void sendUpdatedEntryAtTime(NotificationEntry entry, long postTime) { - setPostTime(entry, postTime); - mBubbleData.notificationEntryUpdated(entry); - } - - private void changeExpandedStateAtTime(boolean shouldBeExpanded, long time) { - when(mTimeSource.currentTimeMillis()).thenReturn(time); - mBubbleData.setExpanded(shouldBeExpanded); + // Assert baseline starting state + assertThat(mBubbleData.hasBubbles()).isFalse(); + assertThat(mBubbleData.isExpanded()).isFalse(); + assertThat(mBubbleData.getSelectedBubble()).isNull(); } @Test @@ -171,8 +129,7 @@ public class BubbleDataTest extends SysuiTestCase { mBubbleData.setListener(mListener); // Test - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); + sendUpdatedEntryAtTime(mEntryA1, 1000); // Verify verify(mListener).onBubbleAdded(eq(mBubbleA1)); @@ -183,9 +140,9 @@ public class BubbleDataTest extends SysuiTestCase { @Test public void testRemoveBubble() { // Setup - mBubbleData.notificationEntryUpdated(mEntryA1); - mBubbleData.notificationEntryUpdated(mEntryA2); - mBubbleData.notificationEntryUpdated(mEntryA3); + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryA3, 3000); mBubbleData.setListener(mListener); // Test @@ -193,515 +150,657 @@ public class BubbleDataTest extends SysuiTestCase { // Verify verify(mListener).onBubbleRemoved(eq(mBubbleA1), eq(BubbleController.DISMISS_USER_GESTURE)); - verify(mListener).onSelectionChanged(eq(mBubbleA2)); verify(mListener).apply(); } + // COLLAPSED / ADD + + /** + * Verifies that the number of bubbles is not allowed to exceed the maximum. The limit is + * enforced by expiring the bubble which was least recently updated (lowest timestamp). + */ @Test - public void test_collapsed_addBubble_atMaxBubbles_expiresLeastActive() { - // Given + public void test_collapsed_addBubble_atMaxBubbles_expiresOldest() { + // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); sendUpdatedEntryAtTime(mEntryA2, 2000); sendUpdatedEntryAtTime(mEntryA3, 3000); sendUpdatedEntryAtTime(mEntryB1, 4000); sendUpdatedEntryAtTime(mEntryB2, 5000); - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); + mBubbleData.setListener(mListener); - // When + // Test sendUpdatedEntryAtTime(mEntryC1, 6000); - - // Then - // A2 is removed. A1 is oldest but is the selected bubble. - assertThat(mBubbleData.getBubbles()).doesNotContain(mBubbleA2); - } - - @Test - public void test_collapsed_expand_whenEmpty_doesNothing() { - assertThat(mBubbleData.hasBubbles()).isFalse(); - changeExpandedStateAtTime(true, 2000L); - - verify(mListener, never()).onExpandedChanged(anyBoolean()); - verify(mListener, never()).apply(); + verify(mListener).onBubbleRemoved(eq(mBubbleA1), eq(DISMISS_AGED)); } - // New bubble while stack is collapsed + /** + * Verifies that new bubbles insert to the left when collapsed, carrying along grouped bubbles. + * <p> + * Placement within the list is based on lastUpdate (post time of the notification), descending + * order (with most recent first). + * + * @see #test_expanded_addBubble_sortAndGrouping_newGroup() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ @Test - public void test_collapsed_addBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); + public void test_collapsed_addBubble_sortAndGrouping() { + // Setup + mBubbleData.setListener(mListener); - // When + // Test sendUpdatedEntryAtTime(mEntryA1, 1000); + verify(mListener, never()).onOrderChanged(anyList()); + + reset(mListener); sendUpdatedEntryAtTime(mEntryB1, 2000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleA1))); + + reset(mListener); sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB2, mBubbleB1, mBubbleA1))); - // Then - // New bubbles move to front when collapsed, bringing bubbles from the same app along - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); + reset(mListener); + sendUpdatedEntryAtTime(mEntryA2, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1))); } - // New bubble while collapsed with ongoing bubble present + /** + * Verifies that new bubbles insert to the left when collapsed, carrying along grouped bubbles. + * Additionally, any bubble which is ongoing is considered "newer" than any non-ongoing bubble. + * <p> + * Because of the ongoing bubble, the new bubble cannot be placed in the first position. This + * causes the 'B' group to remain last, despite having a new button added. + * + * @see #test_expanded_addBubble_sortAndGrouping_newGroup() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ @Test - public void test_collapsed_addBubble_withOngoing() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); + public void test_collapsed_addBubble_sortAndGrouping_withOngoing() { + // Setup + mBubbleData.setListener(mListener); - // When + // Test setOngoing(mEntryA1, true); - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryB2); - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); - - // Then - // New bubbles move to front, but stay behind any ongoing bubbles. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA1, mBubbleA2, mBubbleB2, mBubbleB1)); - } + sendUpdatedEntryAtTime(mEntryA1, 1000); + verify(mListener, never()).onOrderChanged(anyList()); - // Remove the selected bubble (middle bubble), while the stack is collapsed. - @Test - public void test_collapsed_removeBubble_selected() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); + reset(mListener); + sendUpdatedEntryAtTime(mEntryB1, 2000); + verify(mListener, never()).onOrderChanged(eq(listOf(mBubbleA1, mBubbleB1))); - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); + reset(mListener); + sendUpdatedEntryAtTime(mEntryB2, 3000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA1, mBubbleB2, mBubbleB1))); - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); + reset(mListener); + sendUpdatedEntryAtTime(mEntryA2, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA1, mBubbleA2, mBubbleB2, mBubbleB1))); + } - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryB2); + /** + * Verifies that new bubbles become the selected bubble when they appear when the stack is in + * the collapsed state. + * + * @see #test_collapsed_updateBubble_selectionChanges() + * @see #test_collapsed_updateBubble_noSelectionChanges_withOngoing() + */ + @Test + public void test_collapsed_addBubble_selectionChanges() { + // Setup + mBubbleData.setListener(mListener); - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); + // Test + sendUpdatedEntryAtTime(mEntryA1, 1000); + verify(mListener).onSelectionChanged(eq(mBubbleA1)); - mBubbleData.setSelectedBubble(mBubbleB2); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); + reset(mListener); + sendUpdatedEntryAtTime(mEntryB1, 2000); + verify(mListener).onSelectionChanged(eq(mBubbleB1)); - // When - mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE); + reset(mListener); + sendUpdatedEntryAtTime(mEntryB2, 3000); + verify(mListener).onSelectionChanged(eq(mBubbleB2)); - // Then - // (Selection remains in the same position) - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleB1); + reset(mListener); + sendUpdatedEntryAtTime(mEntryA2, 4000); + verify(mListener).onSelectionChanged(eq(mBubbleA2)); } - - // Remove the selected bubble (last bubble), while the stack is collapsed. + /** + * Verifies that while collapsed, the selection will not change if the selected bubble is + * ongoing. It remains the top bubble and as such remains selected. + * + * @see #test_collapsed_addBubble_selectionChanges() + */ @Test - public void test_collapsed_removeSelectedBubble_inLastPosition() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - + public void test_collapsed_addBubble_noSelectionChanges_withOngoing() { + // Setup + setOngoing(mEntryA1, true); sendUpdatedEntryAtTime(mEntryA1, 1000); + assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); + mBubbleData.setListener(mListener); + + // Test sendUpdatedEntryAtTime(mEntryB1, 2000); sendUpdatedEntryAtTime(mEntryB2, 3000); sendUpdatedEntryAtTime(mEntryA2, 4000); - - mBubbleData.setSelectedBubble(mBubbleB1); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE); - - // Then - // (Selection is forced to move to previous) - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleB2); + verify(mListener, never()).onSelectionChanged(any(Bubble.class)); + assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); // selection unchanged } - @Test - public void test_collapsed_addBubble_ongoing() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - // When - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); - - setPostTime(mEntryB2, 3000); - setOngoing(mEntryB2, true); - mBubbleData.notificationEntryUpdated(mEntryB2); - - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); - - // Then - // New bubbles move to front, but stay behind any ongoing bubbles. - // Does not break grouping. (A2 is inserted after B1, even though it's newer). - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - } + // COLLAPSED / REMOVE + /** + * Verifies that groups may reorder when bubbles are removed, while the stack is in the + * collapsed state. + */ @Test - public void test_collapsed_removeBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - + public void test_collapsed_removeBubble_sortAndGrouping() { + // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); sendUpdatedEntryAtTime(mEntryB1, 2000); sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - // When - mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); - // Then - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB1)); + // Test + mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB2, mBubbleB1, mBubbleA1))); } - @Test - public void test_collapsed_updateBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); + /** + * Verifies that onOrderChanged is not called when a bubble is removed if the removal does not + * cause other bubbles to change position. + */ + @Test + public void test_collapsed_removeOldestBubble_doesNotCallOnOrderChanged() { + // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); sendUpdatedEntryAtTime(mEntryB1, 2000); sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - sendUpdatedEntryAtTime(mEntryB2, 5000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); - // Then - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); + // Test + mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE); + verify(mListener, never()).onOrderChanged(anyList()); } + /** + * Verifies that bubble ordering reverts to normal when an ongoing bubble is removed. A group + * which has a newer bubble may move to the front after the ongoing bubble is removed. + */ @Test - public void test_collapsed_updateBubble_withOngoing() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); - - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); - - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryB2); - - setOngoing(mEntryA2, true); - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); - - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); - - // When - setPostTime(mEntryB1, 5000); - mBubbleData.notificationEntryUpdated(mEntryB1); + public void test_collapsed_removeBubble_sortAndGrouping_withOngoing() { + // Setup + setOngoing(mEntryA1, true); + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [A1*, A2, B2, B1] + mBubbleData.setListener(mListener); - // Then - // A2 remains in first position, due to being ongoing. B1 moves before B2, Group A - // remains before group B. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB1, mBubbleB2)); + // Test + mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_NOTIF_CANCEL); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB2, mBubbleB1, mBubbleA2))); } + /** + * Verifies that when the selected bubble is removed with the stack in the collapsed state, + * the selection moves to the next most-recently updated bubble. + */ @Test - public void test_collapse_afterUpdateWhileExpanded() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - + public void test_collapsed_removeBubble_selectionChanges() { + // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); sendUpdatedEntryAtTime(mEntryB1, 2000); sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); - changeExpandedStateAtTime(true, 5000L); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); + // Test + mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_NOTIF_CANCEL); + verify(mListener).onSelectionChanged(eq(mBubbleB2)); + } - sendUpdatedEntryAtTime(mEntryB1, 6000); + // COLLAPSED / UPDATE - // (No reordering while expanded) - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); + /** + * Verifies that bubble and group ordering may change with updates while the stack is in the + * collapsed state. + */ + @Test + public void test_collapsed_updateBubble_orderAndGrouping() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); - // When - changeExpandedStateAtTime(false, 7000L); + // Test + sendUpdatedEntryAtTime(mEntryB1, 5000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleB2, mBubbleA2, mBubbleA1))); - // Then - // A1 moves to front on collapse, since it is the selected bubble (and most recently - // accessed). - // A2 moves next to A1 to maintain grouping. - // B1 moves in front of B2, since it received an update while expanded - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA1, mBubbleA2, mBubbleB1, mBubbleB2)); + reset(mListener); + sendUpdatedEntryAtTime(mEntryA1, 6000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA1, mBubbleA2, mBubbleB1, mBubbleB2))); } + /** + * Verifies that selection tracks the most recently updated bubble while in the collapsed state. + */ @Test - public void test_collapse_afterUpdateWhileExpanded_withOngoing() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - + public void test_collapsed_updateBubble_selectionChanges() { + // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); sendUpdatedEntryAtTime(mEntryB1, 2000); - - setOngoing(mEntryB2, true); sendUpdatedEntryAtTime(mEntryB2, 3000); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A2, A1, B2, B1] + mBubbleData.setListener(mListener); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - - changeExpandedStateAtTime(true, 5000L); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); + // Test + sendUpdatedEntryAtTime(mEntryB1, 5000); + verify(mListener).onSelectionChanged(eq(mBubbleB1)); + reset(mListener); sendUpdatedEntryAtTime(mEntryA1, 6000); - - // No reordering if expanded - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - - // When - changeExpandedStateAtTime(false, 7000L); - - // Then - // B2 remains in first position because it is ongoing. - // B1 remains grouped with B2 - // A1 moves in front of A2, since it is more recently updated (and is selected). - // B1 moves in front of B2, since it has more recent activity. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA1, mBubbleA2)); + verify(mListener).onSelectionChanged(eq(mBubbleA1)); } + /** + * Verifies that selection does not change in response to updates when collapsed, if the + * selected bubble is ongoing. + */ @Test - public void test_collapsed_removeLastBubble_clearsSelectedBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - + public void test_collapsed_updateBubble_noSelectionChanges_withOngoing() { + // Setup + setOngoing(mEntryA1, true); sendUpdatedEntryAtTime(mEntryA1, 1000); sendUpdatedEntryAtTime(mEntryB1, 2000); sendUpdatedEntryAtTime(mEntryB2, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - - mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE); + sendUpdatedEntryAtTime(mEntryA2, 4000); // [A1*, A2, B2, B1] + mBubbleData.setListener(mListener); - assertThat(mBubbleData.getSelectedBubble()).isNull(); + // Test + sendUpdatedEntryAtTime(mEntryB2, 5000); // [A1*, A2, B2, B1] + verify(mListener, never()).onSelectionChanged(any(Bubble.class)); } + /** + * Verifies that a request to expand the stack has no effect if there are no bubbles. + */ @Test - public void test_expanded_addBubble_atMaxBubbles_expiresLeastActive() { - // Given - sendUpdatedEntryAtTime(mEntryA1, 1000); - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - + public void test_collapsed_expansion_whenEmpty_doesNothing() { + assertThat(mBubbleData.hasBubbles()).isFalse(); changeExpandedStateAtTime(true, 2000L); - assertThat(mBubbleData.getSelectedBubble().getLastActivity()).isEqualTo(2000); - sendUpdatedEntryAtTime(mEntryA2, 3000); - sendUpdatedEntryAtTime(mEntryA3, 4000); - sendUpdatedEntryAtTime(mEntryB1, 5000); - sendUpdatedEntryAtTime(mEntryB2, 6000); - sendUpdatedEntryAtTime(mEntryB3, 7000); - - - // Then - // A1 would be removed, but it is selected and expanded, so it should not go away. - // Instead, fall through to removing A2 (the next oldest). - assertThat(mBubbleData.getBubbles()).doesNotContain(mEntryA2); + verify(mListener, never()).onExpandedChanged(anyBoolean()); + verify(mListener, never()).apply(); } @Test - public void test_expanded_removeLastBubble_collapsesStack() { - // Given - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); + public void test_collapsed_removeLastBubble_clearsSelectedBubble() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); + // Test + mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryC1); + // Verify the selection was cleared. + verify(mListener).onSelectionChanged(isNull()); + } - mBubbleData.setExpanded(true); + // EXPANDED / ADD - mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryB1, BubbleController.DISMISS_USER_GESTURE); - mBubbleData.notificationEntryRemoved(mEntryC1, BubbleController.DISMISS_USER_GESTURE); + /** + * Verifies that bubbles added as part of a new group insert before existing groups while + * expanded. + * <p> + * Placement within the list is based on lastUpdate (post time of the notification), descending + * order (with most recent first). + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_expanded_addBubble_sortAndGrouping_newGroup() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); // [B1, A2, A1] + changeExpandedStateAtTime(true, 4000L); + mBubbleData.setListener(mListener); - assertThat(mBubbleData.isExpanded()).isFalse(); - assertThat(mBubbleData.getSelectedBubble()).isNull(); + // Test + sendUpdatedEntryAtTime(mEntryC1, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleC1, mBubbleB1, mBubbleA2, mBubbleA1))); } - // Bubbles do not reorder while expanded + /** + * Verifies that bubbles added as part of a new group insert before existing groups while + * expanded, but not before any groups with ongoing bubbles. + * + * @see #test_collapsed_addBubble_sortAndGrouping_withOngoing() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ @Test - public void test_expanded_selection_collapseToTop() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); - + public void test_expanded_addBubble_sortAndGrouping_newGroup_withOngoing() { + // Setup + setOngoing(mEntryA1, true); sendUpdatedEntryAtTime(mEntryA1, 1000); sendUpdatedEntryAtTime(mEntryA2, 2000); - sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB1, 3000); // [A1*, A2, B1] + changeExpandedStateAtTime(true, 4000L); + mBubbleData.setListener(mListener); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB1, mBubbleA2, mBubbleA1)); + // Test + sendUpdatedEntryAtTime(mEntryC1, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA1, mBubbleA2, mBubbleC1, mBubbleB1))); + } + /** + * Verifies that bubbles added as part of an existing group insert to the beginning of that + * group. The order of groups within the list must not change while in the expanded state. + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_newGroup() + */ + @Test + public void test_expanded_addBubble_sortAndGrouping_existingGroup() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); // [B1, A2, A1] changeExpandedStateAtTime(true, 4000L); + mBubbleData.setListener(mListener); - // regrouping only happens when collapsed (after new or update) or expanded->collapsed - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB1, mBubbleA2, mBubbleA1)); + // Test + sendUpdatedEntryAtTime(mEntryA3, 4000); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleA3, mBubbleA2, mBubbleA1))); + } - changeExpandedStateAtTime(false, 6000L); + // EXPANDED / UPDATE - // A1 is still selected and it's lastAccessed time has been updated - // on collapse, sorting is applied, keeping the selected bubble at the front - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA1, mBubbleA2, mBubbleB1)); + /** + * Verifies that updates to bubbles while expanded do not result in any change to sorting + * or grouping of bubbles or sorting of groups. + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_expanded_updateBubble_sortAndGrouping_noChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1] + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setListener(mListener); + + // Test + sendUpdatedEntryAtTime(mEntryA1, 4000); + verify(mListener, never()).onOrderChanged(anyList()); } - // New bubble from new app while stack is expanded + /** + * Verifies that updates to bubbles while expanded do not result in any change to selection. + * + * @see #test_collapsed_addBubble_selectionChanges() + * @see #test_collapsed_updateBubble_noSelectionChanges_withOngoing() + */ @Test - public void test_expanded_addBubble_newApp() { - // Given + public void test_expanded_updateBubble_noSelectionChanges() { + // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); sendUpdatedEntryAtTime(mEntryA2, 2000); - sendUpdatedEntryAtTime(mEntryA3, 3000); - sendUpdatedEntryAtTime(mEntryB1, 4000); - sendUpdatedEntryAtTime(mEntryB2, 5000); + sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1] + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setListener(mListener); - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); + // Test + sendUpdatedEntryAtTime(mEntryA1, 6000); + sendUpdatedEntryAtTime(mEntryA2, 7000); + sendUpdatedEntryAtTime(mEntryB1, 8000); + verify(mListener, never()).onSelectionChanged(any(Bubble.class)); + } - changeExpandedStateAtTime(true, 6000L); + // EXPANDED / REMOVE - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleA1); - assertThat(mBubbleData.getSelectedBubble().getLastActivity()).isEqualTo(6000L); + /** + * Verifies that removing a bubble while expanded does not result in reordering of groups + * or any of the remaining bubbles. + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_expanded_removeBubble_sortAndGrouping() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); // [B2, B1, A2, A1] + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setListener(mListener); - // regrouping only happens when collapsed (after new or update) or expanded->collapsed - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA3, mBubbleA2, mBubbleA1)); + // Test + mBubbleData.notificationEntryRemoved(mEntryB2, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleA2, mBubbleA1))); + } + + /** + * Verifies that removing the selected bubble while expanded causes another bubble to become + * selected. The replacement selection is the bubble which appears at the same index as the + * previous one, or the previous index if this was the last position. + * + * @see #test_collapsed_addBubble_sortAndGrouping() + * @see #test_expanded_addBubble_sortAndGrouping_existingGroup() + */ + @Test + public void test_expanded_removeBubble_selectionChanges_whenSelectedRemoved() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); + changeExpandedStateAtTime(true, 5000L); + mBubbleData.setSelectedBubble(mBubbleA2); // [B2, B1, ^A2, A1] + mBubbleData.setListener(mListener); - // When - sendUpdatedEntryAtTime(mEntryC1, 7000); + // Test + mBubbleData.notificationEntryRemoved(mEntryA2, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onSelectionChanged(mBubbleA1); - // Then - // A2 is expired. A1 was oldest, but lastActivityTime is reset when expanded, since A1 is - // selected. - // C1 is added at the end since bubbles are expanded. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA3, mBubbleA1, mBubbleC1)); + reset(mListener); + mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onSelectionChanged(mBubbleB1); } - // New bubble from existing app while stack is expanded @Test - public void test_expanded_addBubble_existingApp() { - // Given - sendUpdatedEntryAtTime(mEntryB1, 1000); - sendUpdatedEntryAtTime(mEntryB2, 2000); - sendUpdatedEntryAtTime(mEntryA1, 3000); - sendUpdatedEntryAtTime(mEntryA2, 4000); - sendUpdatedEntryAtTime(mEntryA3, 5000); - - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleB1); + public void test_expandAndCollapse_callsOnExpandedChanged() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); - changeExpandedStateAtTime(true, 6000L); + // Test + changeExpandedStateAtTime(true, 3000L); + verify(mListener).onExpandedChanged(eq(true)); - // B1 is first (newest, since it's just been expanded and is selected) - assertThat(mBubbleData.getSelectedBubble()).isEqualTo(mBubbleB1); - assertThat(mBubbleData.getSelectedBubble().getLastActivity()).isEqualTo(6000L); + reset(mListener); + changeExpandedStateAtTime(false, 4000L); + verify(mListener).onExpandedChanged(eq(false)); + } - // regrouping only happens when collapsed (after new or update) or while collapsing + /** + * Verifies that transitions between the collapsed and expanded state maintain sorting and + * grouping rules. + * <p> + * While collapsing, sorting is applied since no sorting happens while expanded. The resulting + * state is the new expanded ordering. This state is saved and restored if possible when next + * expanded. + * <p> + * When the stack transitions to the collapsed state, the selected bubble is brought to the top. + * Bubbles within the same group should move up with it. + * <p> + * When the stack transitions back to the expanded state, the previous ordering is restored, as + * long as no changes have been made (adds, removes or updates) while in the collapsed state. + */ + @Test + public void test_expansionChanges() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); + sendUpdatedEntryAtTime(mEntryB2, 4000); + changeExpandedStateAtTime(true, 5000L); // [B2=4000, B1=2000, A2=3000, A1=1000] + sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, B1=6000*, A2=3000, A1=1000] + setCurrentTime(7000); + mBubbleData.setSelectedBubble(mBubbleA2); + mBubbleData.setListener(mListener); assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA3, mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); + listOf(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - // When - sendUpdatedEntryAtTime(mEntryB3, 7000); + // Test - // Then - // (B2 is expired, B1 was oldest, but it's lastActivityTime is updated at the point when - // the stack was expanded, since it is the selected bubble. - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA3, mBubbleA2, mBubbleA1, mBubbleB3, mBubbleB1)); + // At this point, B1 has been updated but sorting has not been changed because the + // stack is expanded. When next collapsed, sorting will be applied and saved, just prior + // to moving the selected bubble to the top (first). + // + // In this case, the expected re-expand state will be: [B1, B2, A2*, A1] + // + // That state is restored as long as no changes occur (add/remove/update) while in + // the collapsed state. + // + // collapse -> selected bubble (A2) moves first. + changeExpandedStateAtTime(false, 8000L); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA2, mBubbleA1, mBubbleB1, mBubbleB2))); + + // expand -> "original" order/grouping restored + reset(mListener); + changeExpandedStateAtTime(true, 10000L); + verify(mListener).onOrderChanged(eq(listOf(mBubbleB1, mBubbleB2, mBubbleA2, mBubbleA1))); } - // Updated bubble from existing app while stack is expanded + /** + * When a change occurs while collapsed (any update, add, remove), the previous expanded + * order and grouping becomes invalidated, and the order and grouping when next expanded will + * remain the same as collapsed. + */ @Test - public void test_expanded_updateBubble_existingApp() { + public void test_expansionChanges_withUpdatesWhileCollapsed() { + // Setup sendUpdatedEntryAtTime(mEntryA1, 1000); - sendUpdatedEntryAtTime(mEntryA2, 2000); - sendUpdatedEntryAtTime(mEntryB1, 3000); + sendUpdatedEntryAtTime(mEntryB1, 2000); + sendUpdatedEntryAtTime(mEntryA2, 3000); sendUpdatedEntryAtTime(mEntryB2, 4000); + changeExpandedStateAtTime(true, 5000L); // [B2=4000, B1=2000, A2=3000, A1=1000] + sendUpdatedEntryAtTime(mEntryB1, 6000); // [B2=4000, B1=*6000, A2=3000, A1=1000] + setCurrentTime(7000); + mBubbleData.setSelectedBubble(mBubbleA2); // [B2, B1, ^A2, A1] + mBubbleData.setListener(mListener); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); - mBubbleData.setExpanded(true); - - sendUpdatedEntryAtTime(mEntryA1, 5000); + // Test - // Does not reorder while expanded (for an update). - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleB2, mBubbleB1, mBubbleA2, mBubbleA1)); + // At this point, B1 has been updated but sorting has not been changed because the + // stack is expanded. When next collapsed, sorting will be applied and saved, just prior + // to moving the selected bubble to the top (first). + // + // In this case, the expected re-expand state will be: [B1, B2, A2*, A1] + // + // That state is restored as long as no changes occur (add/remove/update) while in + // the collapsed state. + // + // collapse -> selected bubble (A2) moves first. + changeExpandedStateAtTime(false, 8000L); + verify(mListener).onOrderChanged(eq(listOf(mBubbleA2, mBubbleA1, mBubbleB1, mBubbleB2))); + + // An update occurs, which causes sorting, and this invalidates the previously saved order. + sendUpdatedEntryAtTime(mEntryA2, 9000); + + // No order changes when expanding because the new sorted order remains. + reset(mListener); + changeExpandedStateAtTime(true, 10000L); + verify(mListener, never()).onOrderChanged(anyList()); } @Test - public void test_expanded_updateBubble() { - // Given - assertThat(mBubbleData.hasBubbles()).isFalse(); - assertThat(mBubbleData.isExpanded()).isFalse(); + public void test_expanded_removeLastBubble_collapsesStack() { + // Setup + sendUpdatedEntryAtTime(mEntryA1, 1000); + changeExpandedStateAtTime(true, 2000); + mBubbleData.setListener(mListener); - setPostTime(mEntryA1, 1000); - mBubbleData.notificationEntryUpdated(mEntryA1); + // Test + mBubbleData.notificationEntryRemoved(mEntryA1, BubbleController.DISMISS_USER_GESTURE); + verify(mListener).onExpandedChanged(eq(false)); + } - setPostTime(mEntryB1, 2000); - mBubbleData.notificationEntryUpdated(mEntryB1); + private NotificationEntry createBubbleEntry(int userId, String notifKey, String packageName) { + return createBubbleEntry(userId, notifKey, packageName, 1000); + } - setPostTime(mEntryB2, 3000); - mBubbleData.notificationEntryUpdated(mEntryB2); + private void setPostTime(NotificationEntry entry, long postTime) { + when(entry.notification.getPostTime()).thenReturn(postTime); + } - setPostTime(mEntryA2, 4000); - mBubbleData.notificationEntryUpdated(mEntryA2); + private void setOngoing(NotificationEntry entry, boolean ongoing) { + if (ongoing) { + entry.notification.getNotification().flags |= Notification.FLAG_FOREGROUND_SERVICE; + } else { + entry.notification.getNotification().flags &= ~Notification.FLAG_FOREGROUND_SERVICE; + } + } - mBubbleData.setExpanded(true); - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); + /** + * No ExpandableNotificationRow is required to test BubbleData. This setup is all that is + * required for BubbleData functionality and verification. NotificationTestHelper is used only + * as a convenience to create a Notification w/BubbleMetadata. + */ + private NotificationEntry createBubbleEntry(int userId, String notifKey, String packageName, + long postTime) { + // BubbleMetadata + Notification.BubbleMetadata bubbleMetadata = new Notification.BubbleMetadata.Builder() + .setIntent(mExpandIntent) + .setDeleteIntent(mDeleteIntent) + .setIcon(Icon.createWithResource("", 0)) + .build(); + // Notification -> BubbleMetadata + Notification notification = mNotificationTestHelper.createNotification(false, + null /* groupKey */, bubbleMetadata); - // When - setPostTime(mEntryB1, 5000); - mBubbleData.notificationEntryUpdated(mEntryB1); + // StatusBarNotification + StatusBarNotification sbn = mock(StatusBarNotification.class); + when(sbn.getKey()).thenReturn(notifKey); + when(sbn.getUser()).thenReturn(new UserHandle(userId)); + when(sbn.getPackageName()).thenReturn(packageName); + when(sbn.getPostTime()).thenReturn(postTime); + when(sbn.getNotification()).thenReturn(notification); - // Then - // B1 remains in the same place due to being expanded - assertThat(mBubbleData.getBubbles()).isEqualTo( - ImmutableList.of(mBubbleA2, mBubbleA1, mBubbleB2, mBubbleB1)); + // NotificationEntry -> StatusBarNotification -> Notification -> BubbleMetadata + return new NotificationEntry(sbn); + } + + private void setCurrentTime(long time) { + when(mTimeSource.currentTimeMillis()).thenReturn(time); + } + + private void sendUpdatedEntryAtTime(NotificationEntry entry, long postTime) { + setPostTime(entry, postTime); + mBubbleData.notificationEntryUpdated(entry); + } + + private void changeExpandedStateAtTime(boolean shouldBeExpanded, long time) { + setCurrentTime(time); + mBubbleData.setExpanded(shouldBeExpanded); + } + + /** Syntactic sugar to keep assertions more readable */ + private static <T> List<T> listOf(T... a) { + return ImmutableList.copyOf(a); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java index cd8480505c04..567d192073b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java @@ -17,6 +17,8 @@ package com.android.systemui.bubbles.animation; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.Mockito.verify; import android.content.res.Resources; import android.graphics.Point; @@ -69,14 +71,14 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); testBubblesInCorrectExpandedPositions(); - Mockito.verify(afterExpand).run(); + verify(afterExpand).run(); Runnable afterCollapse = Mockito.mock(Runnable.class); mExpandedController.collapseBackToStack(afterCollapse); waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); testStackedAtPosition(mExpansionPoint.x, mExpansionPoint.y, -1); - Mockito.verify(afterExpand).run(); + verify(afterExpand).run(); } @Test @@ -140,6 +142,78 @@ public class ExpandedAnimationControllerTest extends PhysicsAnimationLayoutTestC testBubblesInCorrectExpandedPositions(); } + @Test + public void testMagnetToDismiss_dismiss() throws InterruptedException { + expand(); + + final View draggedOutView = mViews.get(0); + final Runnable after = Mockito.mock(Runnable.class); + + mExpandedController.prepareForBubbleDrag(draggedOutView); + mExpandedController.dragBubbleOut(draggedOutView, 25, 25); + + // Magnet to dismiss, verify the bubble is at the dismiss target and the callback was + // called. + mExpandedController.magnetBubbleToDismiss( + mViews.get(0), 100 /* velX */, 100 /* velY */, 1000 /* destY */, after); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + verify(after).run(); + assertEquals(1000, mViews.get(0).getTranslationY(), .1f); + + // Dismiss the now-magneted bubble, verify that the callback was called. + final Runnable afterDismiss = Mockito.mock(Runnable.class); + mExpandedController.dismissDraggedOutBubble(afterDismiss); + waitForPropertyAnimations(DynamicAnimation.ALPHA); + verify(after).run(); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + assertEquals(mBubblePadding, mViews.get(1).getTranslationX(), 1f); + } + + @Test + public void testMagnetToDismiss_demagnetizeThenDrag() throws InterruptedException { + expand(); + + final View draggedOutView = mViews.get(0); + final Runnable after = Mockito.mock(Runnable.class); + + mExpandedController.prepareForBubbleDrag(draggedOutView); + mExpandedController.dragBubbleOut(draggedOutView, 25, 25); + + // Magnet to dismiss, verify the bubble is at the dismiss target and the callback was + // called. + mExpandedController.magnetBubbleToDismiss( + draggedOutView, 100 /* velX */, 100 /* velY */, 1000 /* destY */, after); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + verify(after).run(); + assertEquals(1000, mViews.get(0).getTranslationY(), .1f); + + // Demagnetize the bubble towards (25, 25). + mExpandedController.demagnetizeBubbleTo(25 /* x */, 25 /* y */, 100, 100); + + // Start dragging towards (20, 20). + mExpandedController.dragBubbleOut(draggedOutView, 20, 20); + + // Since we just demagnetized, the bubble shouldn't be at (20, 20), it should be animating + // towards it. + assertNotEquals(20, draggedOutView.getTranslationX()); + assertNotEquals(20, draggedOutView.getTranslationY()); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + // Waiting for the animations should result in the bubble ending at (20, 20) since the + // animation end value was updated. + assertEquals(20, draggedOutView.getTranslationX(), 1f); + assertEquals(20, draggedOutView.getTranslationY(), 1f); + + // Drag to (30, 30). + mExpandedController.dragBubbleOut(draggedOutView, 30, 30); + + // It should go there instantly since the animations finished. + assertEquals(30, draggedOutView.getTranslationX(), 1f); + assertEquals(30, draggedOutView.getTranslationY(), 1f); + } + /** Expand the stack and wait for animations to finish. */ private void expand() throws InterruptedException { mExpandedController.expandFromStack(mExpansionPoint, Mockito.mock(Runnable.class)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java index 9fce092ef7ce..a398fba008bb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/PhysicsAnimationLayoutTestCase.java @@ -195,9 +195,11 @@ public class PhysicsAnimationLayoutTestCase extends SysuiTestCase { @Override protected void animateValueForChild(DynamicAnimation.ViewProperty property, View view, - float value, float startVel, long startDelay, Runnable[] afterCallbacks) { + float value, float startVel, long startDelay, float stiffness, + float dampingRatio, Runnable[] afterCallbacks) { mMainThreadHandler.post(() -> super.animateValueForChild( - property, view, value, startVel, startDelay, afterCallbacks)); + property, view, value, startVel, startDelay, stiffness, dampingRatio, + afterCallbacks)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java index 910cee3574dd..b83276bc93da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java @@ -17,6 +17,8 @@ package com.android.systemui.bubbles.animation; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.Mockito.verify; import android.graphics.PointF; import android.testing.AndroidTestingRunner; @@ -33,6 +35,7 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.mockito.Spy; @SmallTest @@ -223,6 +226,59 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase assertEquals(prevStackPos, mStackController.getStackPosition()); } + @Test + public void testMagnetToDismiss_dismiss() throws InterruptedException { + final Runnable after = Mockito.mock(Runnable.class); + + // Magnet to dismiss, verify the stack is at the dismiss target and the callback was + // called. + mStackController.magnetToDismiss(100 /* velX */, 100 /* velY */, 1000 /* destY */, after); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + verify(after).run(); + assertEquals(1000, mViews.get(0).getTranslationY(), .1f); + + // Dismiss the stack, verify that the callback was called. + final Runnable afterImplode = Mockito.mock(Runnable.class); + mStackController.implodeStack(afterImplode); + waitForPropertyAnimations( + DynamicAnimation.ALPHA, DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y); + verify(after).run(); + } + + @Test + public void testMagnetToDismiss_demagnetizeThenDrag() throws InterruptedException { + final Runnable after = Mockito.mock(Runnable.class); + + // Magnet to dismiss, verify the stack is at the dismiss target and the callback was + // called. + mStackController.magnetToDismiss(100 /* velX */, 100 /* velY */, 1000 /* destY */, after); + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + verify(after).run(); + + assertEquals(1000, mViews.get(0).getTranslationY(), .1f); + + // Demagnetize towards (25, 25) and then send a touch event. + mStackController.demagnetizeFromDismissToPoint(25, 25, 0, 0); + waitForLayoutMessageQueue(); + mStackController.moveStackFromTouch(20, 20); + + // Since the stack is demagnetizing, it shouldn't be at the stack position yet. + assertNotEquals(20, mStackController.getStackPosition().x, 1f); + assertNotEquals(20, mStackController.getStackPosition().y, 1f); + + waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y); + + // Once the animation is done it should end at the touch position coordinates. + assertEquals(20, mStackController.getStackPosition().x, 1f); + assertEquals(20, mStackController.getStackPosition().y, 1f); + + mStackController.moveStackFromTouch(30, 30); + + // Touches after the animation are done should change the stack position instantly. + assertEquals(30, mStackController.getStackPosition().x, 1f); + assertEquals(30, mStackController.getStackPosition().y, 1f); + } + /** * Checks every child view to make sure it's stacked at the given coordinates, off to the left * or right side depending on offset multiplier. @@ -249,5 +305,13 @@ public class StackAnimationControllerTest extends PhysicsAnimationLayoutTestCase super.flingThenSpringFirstBubbleWithStackFollowing( property, vel, friction, spring, finalPosition)); } + + @Override + protected void springFirstBubbleWithStackFollowing(DynamicAnimation.ViewProperty property, + SpringForce spring, float vel, float finalPosition) { + mMainThreadHandler.post(() -> + super.springFirstBubbleWithStackFollowing( + property, spring, vel, finalPosition)); + } } } diff --git a/proto/src/metrics_constants/metrics_constants.proto b/proto/src/metrics_constants/metrics_constants.proto index fa62f64d59ca..e30e166a46b5 100644 --- a/proto/src/metrics_constants/metrics_constants.proto +++ b/proto/src/metrics_constants/metrics_constants.proto @@ -4191,6 +4191,8 @@ message MetricsEvent { // Tag FIELD_AUTOFILL_COMPAT_MODE: package is being autofilled on compatibility mode. // Tag FIELD_AUTOFILL_NUMBER_REQUESTS: number of requests made to the service (each request // is logged by a separate AUTOFILL_REQUEST metric) + // NOTE: starting on OS Q, it also added the following fields: + // TAg FIELD_AUTOFILL_AUGMENTED_ONLY: if the session was used just for augmented autofill AUTOFILL_SESSION_FINISHED = 919; // meta-event: a reader has checkpointed the log here. @@ -7243,8 +7245,46 @@ message MetricsEvent { // OS: Q ACTION_DIRECT_SHARE_TARGETS_LOADED_CHOOSER_SERVICE = 1719; - // ---- Skipping ahead to avoid conflicts between master and release branches. + // Field indicating that an autofill session was created just for augmented autofill purposes. + // OS: Q + // Value: 1 for true, absent when false + FIELD_AUTOFILL_AUGMENTED_ONLY = 1720; + + // The augmented autofill service set its whitelisted packages and activities. + // OS: Q + // Tag FIELD_AUTOFILL_SERVICE: Package of the augmented autofill service that processed the + // request + // Tag FIELD_AUTOFILL_NUMBER_PACKAGES: Number of whitelisted packages. + // Tag FIELD_AUTOFILL_NUMBER_ACTIVITIES: Number of whitelisted activities. + AUTOFILL_AUGMENTED_WHITELIST_REQUEST = 1721; + + // Generic field used to indicate the number of packages in an Autofill metric (typically a + // whitelist request). + // OS: Q + FIELD_AUTOFILL_NUMBER_PACKAGES = 1722; + // Generic field used to indicate the number of activities in an Autofill metric (typically a + // whitelist request). + // OS: Q + FIELD_AUTOFILL_NUMBER_ACTIVITIES = 1723; + + // Reports the result of a request made to the augmented autofill service + // OS: Q + // Type TYPE_UNKNOWN: if the type of response could not be determined + // Type TYPE_SUCCESS: service called onSucess() passing null + // Type TYPE_OPEN: service shown the UI + // Type TYPE_CLOSE: service hid the UI + // Type TYPE_ERROR: service timed out responding + + // Tag FIELD_CLASS_NAME: Class name of the activity that is autofilled. + // Tag FIELD_AUTOFILL_SERVICE: Package of the augmented autofill service that processed the + // request + // Tag FIELD_AUTOFILL_SESSION_ID: id of the autofill session associated with this metric + // Tag FIELD_AUTOFILL_DURATION: how long it took (in ms) to the service to respond, or -1 if the + // type of response could not be determined + AUTOFILL_AUGMENTED_RESPONSE = 1724; + + // ---- Skipping ahead to avoid conflicts between master and release branches. // OPEN: Settings > System > Gestures > Global Actions Panel // CATEGORY: SETTINGS // OS: Q diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 386dec472019..1bd5201f5b26 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -20,8 +20,8 @@ import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.view.autofill.AutofillManager.ACTION_START_SESSION; import static android.view.autofill.AutofillManager.FLAG_ADD_CLIENT_ENABLED; import static android.view.autofill.AutofillManager.FLAG_ADD_CLIENT_ENABLED_FOR_AUGMENTED_AUTOFILL_ONLY; -import static android.view.autofill.AutofillManager.FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY; import static android.view.autofill.AutofillManager.NO_SESSION; +import static android.view.autofill.AutofillManager.RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY; import static com.android.server.autofill.Helper.sDebug; import static com.android.server.autofill.Helper.sVerbose; @@ -283,7 +283,7 @@ final class AutofillManagerServiceImpl * * @return {@code long} whose right-most 32 bits represent the session id (which is always * non-negative), and the left-most contains extra flags (currently either {@code 0} or - * {@link AutofillManager#FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY}). + * {@link AutofillManager#RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY}). */ @GuardedBy("mLock") long startSessionLocked(@NonNull IBinder activityToken, int taskId, int uid, @@ -357,7 +357,8 @@ final class AutofillManagerServiceImpl if (forAugmentedAutofillOnly) { // Must embed the flag in the response, at the high-end side of the long. // (session is always positive, so we don't have to worry about the signal bit) - final long extraFlags = ((long) FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) << 32; + final long extraFlags = + ((long) RECEIVER_FLAG_SESSION_FOR_AUGMENTED_AUTOFILL_ONLY) << 32; final long result = extraFlags | newSession.id; return result; } else { @@ -1051,6 +1052,14 @@ final class AutofillManagerServiceImpl } } + @GuardedBy("mLock") + void destroySessionsForAugmentedAutofillOnlyLocked() { + final int sessionCount = mSessions.size(); + for (int i = sessionCount - 1; i >= 0; i--) { + mSessions.valueAt(i).forceRemoveSelfIfForAugmentedAutofillOnlyLocked(); + } + } + // TODO(b/64940307): remove this method if SaveUI is refactored to be attached on activities @GuardedBy("mLock") void destroyFinishedSessionsLocked() { @@ -1070,9 +1079,18 @@ final class AutofillManagerServiceImpl @GuardedBy("mLock") void listSessionsLocked(ArrayList<String> output) { final int numSessions = mSessions.size(); + if (numSessions <= 0) return; + + final String fmt = "%d:%s:%s"; for (int i = 0; i < numSessions; i++) { - output.add((mInfo != null ? mInfo.getServiceInfo().getComponentName() - : null) + ":" + mSessions.keyAt(i)); + final int id = mSessions.keyAt(i); + final String service = mInfo == null + ? "no_svc" + : mInfo.getServiceInfo().getComponentName().flattenToShortString(); + final String augmentedService = mRemoteAugmentedAutofillServiceInfo == null + ? "no_aug" + : mRemoteAugmentedAutofillServiceInfo.getComponentName().flattenToShortString(); + output.add(String.format(fmt, id, service, augmentedService)); } } @@ -1136,6 +1154,7 @@ final class AutofillManagerServiceImpl Slog.v(TAG, "updateRemoteAugmentedAutofillService(): " + "destroying old remote service"); } + destroySessionsForAugmentedAutofillOnlyLocked(); mRemoteAugmentedAutofillService.destroy(); mRemoteAugmentedAutofillService = null; mRemoteAugmentedAutofillServiceInfo = null; @@ -1177,8 +1196,8 @@ final class AutofillManagerServiceImpl * @return whether caller UID is the augmented autofill service for the user */ @GuardedBy("mLock") - boolean setAugmentedAutofillWhitelistLocked(List<String> packages, - List<ComponentName> activities, int callingUid) { + boolean setAugmentedAutofillWhitelistLocked(@Nullable List<String> packages, + @Nullable List<ComponentName> activities, int callingUid) { if (!isCalledByAugmentedAutofillServiceLocked("setAugmentedAutofillWhitelistLocked", callingUid)) { @@ -1189,8 +1208,25 @@ final class AutofillManagerServiceImpl + activities + ")"); } whitelistForAugmentedAutofillPackages(packages, activities); + final String serviceName; + if (mRemoteAugmentedAutofillServiceInfo != null) { + serviceName = mRemoteAugmentedAutofillServiceInfo.getComponentName() + .flattenToShortString(); + } else { + Slog.e(TAG, "setAugmentedAutofillWhitelistLocked(): no service"); + serviceName = "N/A"; + } + + final LogMaker log = new LogMaker(MetricsEvent.AUTOFILL_AUGMENTED_WHITELIST_REQUEST) + .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, serviceName); + if (packages != null) { + log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_PACKAGES, packages.size()); + } + if (activities != null) { + log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_ACTIVITIES, activities.size()); + } + mMetricsLogger.write(log); - // TODO(b/122858578): log metrics return true; } @@ -1233,7 +1269,6 @@ final class AutofillManagerServiceImpl } /** - * * @throws IllegalArgumentException if packages or components are empty. */ private void whitelistForAugmentedAutofillPackages(@Nullable List<String> packages, diff --git a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java index 609904b32230..d2b71e591b22 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteAugmentedAutofillService.java @@ -16,6 +16,8 @@ package com.android.server.autofill; +import static android.service.autofill.augmented.Helper.logResponse; + import static com.android.server.autofill.Helper.sDebug; import static com.android.server.autofill.Helper.sVerbose; @@ -43,6 +45,7 @@ import android.view.autofill.AutofillValue; import android.view.autofill.IAutoFillManagerClient; import com.android.internal.infra.AbstractSinglePendingRequestRemoteService; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.os.IResultReceiver; final class RemoteAugmentedAutofillService @@ -173,6 +176,7 @@ final class RemoteAugmentedAutofillService private final @Nullable AutofillValue mFocusedValue; private final @NonNull IAutoFillManagerClient mClient; private final @NonNull ComponentName mActivityComponent; + private final int mSessionId; private final int mTaskId; private final long mRequestTime = SystemClock.elapsedRealtime(); private final @NonNull IFillCallback mCallback; @@ -184,6 +188,7 @@ final class RemoteAugmentedAutofillService @Nullable AutofillValue focusedValue) { super(service, sessionId); mClient = client; + mSessionId = sessionId; mTaskId = taskId; mActivityComponent = activityComponent; mFocusedId = focusedId; @@ -283,6 +288,8 @@ final class RemoteAugmentedAutofillService remoteService.dispatchOnFillTimeout(cancellation); } finish(); + logResponse(MetricsEvent.TYPE_ERROR, remoteService.getComponentName().getPackageName(), + mActivityComponent, mSessionId, remoteService.mRequestTimeoutMs); } @Override diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 1a0353cde8ce..66b5437f0a7d 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -570,7 +570,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState private void requestNewFillResponseLocked(@NonNull ViewState viewState, int newState, int flags) { if (mForAugmentedAutofillOnly) { - // TODO(b/122858578): log metrics if (sVerbose) { Slog.v(TAG, "requestNewFillResponse(): triggering augmented autofill instead " + "(mForAugmentedAutofillOnly=" + mForAugmentedAutofillOnly @@ -2408,7 +2407,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } - if (mAugmentedAutofillableIds != null && mAugmentedAutofillableIds.contains(id)) { + if ((flags & FLAG_MANUAL_REQUEST) == 0 && mAugmentedAutofillableIds != null + && mAugmentedAutofillableIds.contains(id)) { // View was already reported when server could not handle a response, but it // triggered augmented autofill @@ -2539,7 +2539,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState try { if (mHasCallback) { mClient.notifyNoFillUi(id, mCurrentViewId, sessionFinishedState); - } else if (sessionFinishedState != 0) { + } else if (sessionFinishedState != AutofillManager.STATE_UNKNOWN) { mClient.setSessionFinished(sessionFinishedState, autofillableIds); } } catch (RemoteException e) { @@ -2694,6 +2694,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + "it can be augmented. AutofillableIds: " + autofillableIds); } mAugmentedAutofillableIds = autofillableIds; + try { + mClient.setState(AutofillManager.SET_STATE_FLAG_FOR_AUTOFILL_ONLY); + } catch (RemoteException e) { + Slog.e(TAG, "Error setting client to autofill-only", e); + } } } @@ -3253,6 +3258,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUMBER_AUGMENTED_REQUESTS, totalAugmentedRequests); } + if (mForAugmentedAutofillOnly) { + log.addTaggedData(MetricsEvent.FIELD_AUTOFILL_AUGMENTED_ONLY, 1); + } mMetricsLogger.write(log); return mRemoteFillService; @@ -3268,6 +3276,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } @GuardedBy("mLock") + void forceRemoveSelfIfForAugmentedAutofillOnlyLocked() { + if (sVerbose) { + Slog.v(TAG, "forceRemoveSelfIfForAugmentedAutofillOnly(" + this.id + "): " + + mForAugmentedAutofillOnly); + } + if (!mForAugmentedAutofillOnly) return; + + forceRemoveSelfLocked(); + } + + @GuardedBy("mLock") void forceRemoveSelfLocked(int clientState) { if (sVerbose) Slog.v(TAG, "forceRemoveSelfLocked(): " + mPendingSaveUi); diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java index 4399e4267fda..67c3d01cb86b 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java @@ -54,6 +54,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.view.contentcapture.ContentCaptureCondition; import android.view.contentcapture.DataRemovalRequest; @@ -552,6 +553,39 @@ final class ContentCapturePerUserService + " for user " + mUserId); } mMaster.mGlobalContentCaptureOptions.setWhitelist(mUserId, packages, activities); + + // Must disable session that are not the whitelist anymore... + final int numSessions = mSessions.size(); + if (numSessions <= 0) return; + + // ...but without holding the lock on mGlobalContentCaptureOptions + final SparseBooleanArray blacklistedSessions = new SparseBooleanArray(numSessions); + + for (int i = 0; i < numSessions; i++) { + final ContentCaptureServerSession session = mSessions.valueAt(i); + final boolean whitelisted = mMaster.mGlobalContentCaptureOptions + .isWhitelisted(mUserId, session.appComponentName); + if (!whitelisted) { + final int sessionId = mSessions.keyAt(i); + if (mMaster.debug) { + Slog.d(TAG, "marking session " + sessionId + " (" + session.appComponentName + + ") for un-whitelisting"); + } + blacklistedSessions.append(sessionId, true); + } + } + final int numBlacklisted = blacklistedSessions.size(); + + if (numBlacklisted <= 0) return; + + synchronized (mLock) { + for (int i = 0; i < numBlacklisted; i++) { + final int sessionId = blacklistedSessions.keyAt(i); + if (mMaster.debug) Slog.d(TAG, "un-whitelisting " + sessionId); + final ContentCaptureServerSession session = mSessions.get(sessionId); + session.setContentCaptureEnabledLocked(false); + } + } } @Override diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java index 2643db1d5851..aa63e40747ee 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureServerSession.java @@ -72,6 +72,8 @@ final class ContentCaptureServerSession { private final Object mLock; + public final ComponentName appComponentName; + ContentCaptureServerSession(@NonNull Object lock, @NonNull IBinder activityToken, @NonNull ContentCapturePerUserService service, @NonNull ComponentName appComponentName, @NonNull IResultReceiver sessionStateReceiver, int taskId, int displayId, int sessionId, @@ -79,6 +81,7 @@ final class ContentCaptureServerSession { Preconditions.checkArgument(sessionId != NO_SESSION_ID); mLock = lock; mActivityToken = activityToken; + this.appComponentName = appComponentName; mService = service; mId = sessionId; mUid = uid; @@ -228,6 +231,7 @@ final class ContentCaptureServerSession { pw.print(prefix); pw.print("uid: "); pw.print(mUid); pw.println(); pw.print(prefix); pw.print("context: "); mContentCaptureContext.dump(pw); pw.println(); pw.print(prefix); pw.print("activity token: "); pw.println(mActivityToken); + pw.print(prefix); pw.print("app component: "); pw.println(appComponentName); pw.print(prefix); pw.print("has autofill callback: "); } diff --git a/services/core/java/com/android/server/IpSecService.java b/services/core/java/com/android/server/IpSecService.java index 2055b64483d9..fe22dcda9683 100644 --- a/services/core/java/com/android/server/IpSecService.java +++ b/services/core/java/com/android/server/IpSecService.java @@ -30,6 +30,7 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.NonNull; import android.app.AppOpsManager; import android.content.Context; +import android.content.pm.PackageManager; import android.net.IIpSecService; import android.net.INetd; import android.net.IpSecAlgorithm; @@ -1276,7 +1277,7 @@ public class IpSecService extends IIpSecService.Stub { public synchronized IpSecTunnelInterfaceResponse createTunnelInterface( String localAddr, String remoteAddr, Network underlyingNetwork, IBinder binder, String callingPackage) { - enforceTunnelPermissions(callingPackage); + enforceTunnelFeatureAndPermissions(callingPackage); checkNotNull(binder, "Null Binder passed to createTunnelInterface"); checkNotNull(underlyingNetwork, "No underlying network was specified"); checkInetAddress(localAddr); @@ -1362,7 +1363,7 @@ public class IpSecService extends IIpSecService.Stub { @Override public synchronized void addAddressToTunnelInterface( int tunnelResourceId, LinkAddress localAddr, String callingPackage) { - enforceTunnelPermissions(callingPackage); + enforceTunnelFeatureAndPermissions(callingPackage); UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); // Get tunnelInterface record; if no such interface is found, will throw @@ -1391,7 +1392,7 @@ public class IpSecService extends IIpSecService.Stub { @Override public synchronized void removeAddressFromTunnelInterface( int tunnelResourceId, LinkAddress localAddr, String callingPackage) { - enforceTunnelPermissions(callingPackage); + enforceTunnelFeatureAndPermissions(callingPackage); UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); // Get tunnelInterface record; if no such interface is found, will throw @@ -1420,7 +1421,7 @@ public class IpSecService extends IIpSecService.Stub { @Override public synchronized void deleteTunnelInterface( int resourceId, String callingPackage) throws RemoteException { - enforceTunnelPermissions(callingPackage); + enforceTunnelFeatureAndPermissions(callingPackage); UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid()); releaseResource(userRecord.mTunnelInterfaceRecords, resourceId); } @@ -1549,7 +1550,12 @@ public class IpSecService extends IIpSecService.Stub { private static final String TUNNEL_OP = AppOpsManager.OPSTR_MANAGE_IPSEC_TUNNELS; - private void enforceTunnelPermissions(String callingPackage) { + private void enforceTunnelFeatureAndPermissions(String callingPackage) { + if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IPSEC_TUNNELS)) { + throw new UnsupportedOperationException( + "IPsec Tunnel Mode requires PackageManager.FEATURE_IPSEC_TUNNELS"); + } + checkNotNull(callingPackage, "Null calling package cannot create IpSec tunnels"); switch (getAppOpsManager().noteOp(TUNNEL_OP, Binder.getCallingUid(), callingPackage)) { case AppOpsManager.MODE_DEFAULT: @@ -1621,7 +1627,7 @@ public class IpSecService extends IIpSecService.Stub { IpSecConfig c, IBinder binder, String callingPackage) throws RemoteException { checkNotNull(c); if (c.getMode() == IpSecTransform.MODE_TUNNEL) { - enforceTunnelPermissions(callingPackage); + enforceTunnelFeatureAndPermissions(callingPackage); } checkIpSecConfig(c); checkNotNull(binder, "Null Binder passed to createTransform"); @@ -1729,7 +1735,7 @@ public class IpSecService extends IIpSecService.Stub { public synchronized void applyTunnelModeTransform( int tunnelResourceId, int direction, int transformResourceId, String callingPackage) throws RemoteException { - enforceTunnelPermissions(callingPackage); + enforceTunnelFeatureAndPermissions(callingPackage); checkDirection(direction); int callingUid = Binder.getCallingUid(); diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java index 103a0c7bafdb..32671144aee3 100644 --- a/services/core/java/com/android/server/PackageWatchdog.java +++ b/services/core/java/com/android/server/PackageWatchdog.java @@ -684,13 +684,13 @@ public class PackageWatchdog { } } - /** Adds a {@link DeviceConfig#OnPropertyChangedListener}. */ + /** Adds a {@link DeviceConfig#OnPropertiesChangedListener}. */ private void setPropertyChangedListenerLocked() { - DeviceConfig.addOnPropertyChangedListener( + DeviceConfig.addOnPropertiesChangedListener( DeviceConfig.NAMESPACE_ROLLBACK, mContext.getMainExecutor(), - (namespace, name, value) -> { - if (!DeviceConfig.NAMESPACE_ROLLBACK.equals(namespace)) { + (properties) -> { + if (!DeviceConfig.NAMESPACE_ROLLBACK.equals(properties.getNamespace())) { return; } updateConfigs(); diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index 28bc34859e6c..1a6faecaecfd 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -201,7 +201,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private int[] mDataConnectionNetworkType; - private int mOtaspMode = TelephonyManager.OTASP_UNKNOWN; + private int[] mOtaspMode; private ArrayList<List<CellInfo>> mCellInfo = null; @@ -209,13 +209,12 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private Map<Integer, List<EmergencyNumber>> mEmergencyNumberList; - private CallQuality mCallQuality = new CallQuality(); + private CallQuality[] mCallQuality; - private CallAttributes mCallAttributes = new CallAttributes(new PreciseCallState(), - TelephonyManager.NETWORK_TYPE_UNKNOWN, new CallQuality()); + private CallAttributes[] mCallAttributes; // network type of the call associated with the mCallAttributes and mCallQuality - private int mCallNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; + private int[] mCallNetworkType; private int[] mSrvccState; @@ -223,19 +222,19 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private int mDefaultPhoneId = SubscriptionManager.INVALID_PHONE_INDEX; - private int mRingingCallState = PreciseCallState.PRECISE_CALL_STATE_IDLE; + private int[] mRingingCallState; - private int mForegroundCallState = PreciseCallState.PRECISE_CALL_STATE_IDLE; + private int[] mForegroundCallState; - private int mBackgroundCallState = PreciseCallState.PRECISE_CALL_STATE_IDLE; + private int[] mBackgroundCallState; - private PreciseCallState mPreciseCallState = new PreciseCallState(); + private PreciseCallState[] mPreciseCallState; - private int mCallDisconnectCause = DisconnectCause.NOT_VALID; + private int[] mCallDisconnectCause; private List<ImsReasonInfo> mImsReasonInfo = null; - private int mCallPreciseDisconnectCause = PreciseDisconnectCause.NOT_VALID; + private int[] mCallPreciseDisconnectCause; private boolean mCarrierNetworkChangeState = false; @@ -250,8 +249,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private final LocalLog mListenLog = new LocalLog(100); - private PreciseDataConnectionState mPreciseDataConnectionState = - new PreciseDataConnectionState(); + private PreciseDataConnectionState[] mPreciseDataConnectionState; // Nothing here yet, but putting it here in case we want to add more in the future. static final int ENFORCE_COARSE_LOCATION_PERMISSION_MASK = 0; @@ -342,7 +340,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { Integer newDefaultSubIdObj = new Integer(intent.getIntExtra( PhoneConstants.SUBSCRIPTION_KEY, SubscriptionManager.getDefaultSubscriptionId())); - int newDefaultPhoneId = intent.getIntExtra(PhoneConstants.SLOT_KEY, + int newDefaultPhoneId = intent.getIntExtra(PhoneConstants.PHONE_KEY, SubscriptionManager.getPhoneId(mDefaultSubId)); if (DBG) { log("onReceive:current mDefaultSubId=" + mDefaultSubId @@ -389,10 +387,21 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mMessageWaiting = new boolean[numPhones]; mCallForwarding = new boolean[numPhones]; mCellLocation = new Bundle[numPhones]; - mCellInfo = new ArrayList<List<CellInfo>>(); mSrvccState = new int[numPhones]; - mImsReasonInfo = new ArrayList<ImsReasonInfo>(); - mPhysicalChannelConfigs = new ArrayList<List<PhysicalChannelConfig>>(); + mOtaspMode = new int[numPhones]; + mPreciseCallState = new PreciseCallState[numPhones]; + mForegroundCallState = new int[numPhones]; + mBackgroundCallState = new int[numPhones]; + mRingingCallState = new int[numPhones]; + mCallDisconnectCause = new int[numPhones]; + mCallPreciseDisconnectCause = new int[numPhones]; + mCallQuality = new CallQuality[numPhones]; + mCallNetworkType = new int[numPhones]; + mCallAttributes = new CallAttributes[numPhones]; + mPreciseDataConnectionState = new PreciseDataConnectionState[numPhones]; + mCellInfo = new ArrayList<>(); + mImsReasonInfo = new ArrayList<>(); + mPhysicalChannelConfigs = new ArrayList<>(); mEmergencyNumberList = new HashMap<>(); for (int i = 0; i < numPhones; i++) { mCallState[i] = TelephonyManager.CALL_STATE_IDLE; @@ -410,7 +419,19 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCellInfo.add(i, null); mImsReasonInfo.add(i, null); mSrvccState[i] = TelephonyManager.SRVCC_STATE_HANDOVER_NONE; - mPhysicalChannelConfigs.add(i, new ArrayList<PhysicalChannelConfig>()); + mPhysicalChannelConfigs.add(i, new ArrayList<>()); + mOtaspMode[i] = TelephonyManager.OTASP_UNKNOWN; + mCallDisconnectCause[i] = DisconnectCause.NOT_VALID; + mCallPreciseDisconnectCause[i] = PreciseDisconnectCause.NOT_VALID; + mCallQuality[i] = new CallQuality(); + mCallAttributes[i] = new CallAttributes(new PreciseCallState(), + TelephonyManager.NETWORK_TYPE_UNKNOWN, new CallQuality()); + mCallNetworkType[i] = TelephonyManager.NETWORK_TYPE_UNKNOWN; + mPreciseCallState[i] = new PreciseCallState(); + mRingingCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE; + mForegroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE; + mBackgroundCallState[i] = PreciseCallState.PRECISE_CALL_STATE_IDLE; + mPreciseDataConnectionState[i] = new PreciseDataConnectionState(); } // Note that location can be null for non-phone builds like @@ -731,7 +752,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if ((events & PhoneStateListener.LISTEN_OTASP_CHANGED) != 0) { try { - r.callback.onOtaspChanged(mOtaspMode); + r.callback.onOtaspChanged(mOtaspMode[phoneId]); } catch (RemoteException ex) { remove(r.binder); } @@ -749,15 +770,15 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if ((events & PhoneStateListener.LISTEN_PRECISE_CALL_STATE) != 0) { try { - r.callback.onPreciseCallStateChanged(mPreciseCallState); + r.callback.onPreciseCallStateChanged(mPreciseCallState[phoneId]); } catch (RemoteException ex) { remove(r.binder); } } if ((events & PhoneStateListener.LISTEN_CALL_DISCONNECT_CAUSES) != 0) { try { - r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause, - mCallPreciseDisconnectCause); + r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause[phoneId], + mCallPreciseDisconnectCause[phoneId]); } catch (RemoteException ex) { remove(r.binder); } @@ -772,7 +793,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if ((events & PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE) != 0) { try { r.callback.onPreciseDataConnectionStateChanged( - mPreciseDataConnectionState); + mPreciseDataConnectionState[phoneId]); } catch (RemoteException ex) { remove(r.binder); } @@ -854,7 +875,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } if ((events & PhoneStateListener.LISTEN_CALL_ATTRIBUTES_CHANGED) != 0) { try { - r.callback.onCallAttributesChanged(mCallAttributes); + r.callback.onCallAttributesChanged(mCallAttributes[phoneId]); } catch (RemoteException ex) { remove(r.binder); } @@ -1392,12 +1413,14 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { LinkProperties linkProperties, NetworkCapabilities networkCapabilities, int networkType, boolean roaming) { - notifyDataConnectionForSubscriber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, state, + notifyDataConnectionForSubscriber(SubscriptionManager.DEFAULT_PHONE_INDEX, + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, state, isDataAllowed, apn, apnType, linkProperties, networkCapabilities, networkType, roaming); } - public void notifyDataConnectionForSubscriber(int subId, int state, boolean isDataAllowed, + public void notifyDataConnectionForSubscriber(int phoneId, int subId, int state, + boolean isDataAllowed, String apn, String apnType, LinkProperties linkProperties, NetworkCapabilities networkCapabilities, int networkType, boolean roaming) { @@ -1410,7 +1433,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { + "' apn='" + apn + "' apnType=" + apnType + " networkType=" + networkType + " mRecords.size()=" + mRecords.size()); } - int phoneId = SubscriptionManager.getPhoneId(subId); synchronized (mRecords) { if (validatePhoneId(phoneId)) { // We only call the callback when the change is for default APN type. @@ -1425,8 +1447,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mLocalLog.log(str); for (Record r : mRecords) { if (r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_DATA_CONNECTION_STATE) && - idMatch(r.subId, subId, phoneId)) { + PhoneStateListener.LISTEN_DATA_CONNECTION_STATE) + && idMatch(r.subId, subId, phoneId)) { try { if (DBG) { log("Notify data connection state changed on sub: " + subId); @@ -1442,15 +1464,17 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mDataConnectionState[phoneId] = state; mDataConnectionNetworkType[phoneId] = networkType; } - mPreciseDataConnectionState = new PreciseDataConnectionState(state, networkType, + mPreciseDataConnectionState[phoneId] = new PreciseDataConnectionState( + state, networkType, ApnSetting.getApnTypesBitmaskFromString(apnType), apn, linkProperties, DataFailCause.NONE); for (Record r : mRecords) { if (r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE)) { + PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE) + && idMatch(r.subId, subId, phoneId)) { try { r.callback.onPreciseDataConnectionStateChanged( - mPreciseDataConnectionState); + mPreciseDataConnectionState[phoneId]); } catch (RemoteException ex) { mRemoveList.add(r.binder); } @@ -1466,11 +1490,12 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } public void notifyDataConnectionFailed(String apnType) { - notifyDataConnectionFailedForSubscriber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, + notifyDataConnectionFailedForSubscriber(SubscriptionManager.DEFAULT_PHONE_INDEX, + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, apnType); } - public void notifyDataConnectionFailedForSubscriber(int subId, String apnType) { + public void notifyDataConnectionFailedForSubscriber(int phoneId, int subId, String apnType) { if (!checkNotifyPermission("notifyDataConnectionFailed()")) { return; } @@ -1479,20 +1504,25 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { + " apnType=" + apnType); } synchronized (mRecords) { - mPreciseDataConnectionState = new PreciseDataConnectionState( - TelephonyManager.DATA_UNKNOWN,TelephonyManager.NETWORK_TYPE_UNKNOWN, - ApnSetting.getApnTypesBitmaskFromString(apnType), null, null, - DataFailCause.NONE); - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE)) { - try { - r.callback.onPreciseDataConnectionStateChanged(mPreciseDataConnectionState); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + if (validatePhoneId(phoneId)) { + mPreciseDataConnectionState[phoneId] = new PreciseDataConnectionState( + TelephonyManager.DATA_UNKNOWN,TelephonyManager.NETWORK_TYPE_UNKNOWN, + ApnSetting.getApnTypesBitmaskFromString(apnType), null, null, + DataFailCause.NONE); + for (Record r : mRecords) { + if (r.matchPhoneStateListenerEvent( + PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onPreciseDataConnectionStateChanged( + mPreciseDataConnectionState[phoneId]); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } } } + handleRemoveListLocked(); } broadcastDataConnectionFailed(apnType, subId); @@ -1539,18 +1569,22 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } - public void notifyOtaspChanged(int otaspMode) { + public void notifyOtaspChanged(int subId, int otaspMode) { if (!checkNotifyPermission("notifyOtaspChanged()" )) { return; } + int phoneId = SubscriptionManager.getPhoneId(subId); synchronized (mRecords) { - mOtaspMode = otaspMode; - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent(PhoneStateListener.LISTEN_OTASP_CHANGED)) { - try { - r.callback.onOtaspChanged(otaspMode); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + if (validatePhoneId(phoneId)) { + mOtaspMode[phoneId] = otaspMode; + for (Record r : mRecords) { + if (r.matchPhoneStateListenerEvent(PhoneStateListener.LISTEN_OTASP_CHANGED) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onOtaspChanged(otaspMode); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } } } @@ -1558,49 +1592,55 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } - public void notifyPreciseCallState(int ringingCallState, int foregroundCallState, - int backgroundCallState, int phoneId) { + public void notifyPreciseCallState(int phoneId, int subId, int ringingCallState, + int foregroundCallState, int backgroundCallState) { if (!checkNotifyPermission("notifyPreciseCallState()")) { return; } synchronized (mRecords) { - mRingingCallState = ringingCallState; - mForegroundCallState = foregroundCallState; - mBackgroundCallState = backgroundCallState; - mPreciseCallState = new PreciseCallState(ringingCallState, foregroundCallState, - backgroundCallState, - DisconnectCause.NOT_VALID, - PreciseDisconnectCause.NOT_VALID); - boolean notifyCallAttributes = true; - if (mCallQuality == null) { - log("notifyPreciseCallState: mCallQuality is null, skipping call attributes"); - notifyCallAttributes = false; - } else { - // If the precise call state is no longer active, reset the call network type and - // call quality. - if (mPreciseCallState.getForegroundCallState() - != PreciseCallState.PRECISE_CALL_STATE_ACTIVE) { - mCallNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN; - mCallQuality = new CallQuality(); + if (validatePhoneId(phoneId)) { + mRingingCallState[phoneId] = ringingCallState; + mForegroundCallState[phoneId] = foregroundCallState; + mBackgroundCallState[phoneId] = backgroundCallState; + mPreciseCallState[phoneId] = new PreciseCallState( + ringingCallState, foregroundCallState, + backgroundCallState, + DisconnectCause.NOT_VALID, + PreciseDisconnectCause.NOT_VALID); + boolean notifyCallAttributes = true; + if (mCallQuality == null) { + log("notifyPreciseCallState: mCallQuality is null, " + + "skipping call attributes"); + notifyCallAttributes = false; + } else { + // If the precise call state is no longer active, reset the call network type + // and call quality. + if (mPreciseCallState[phoneId].getForegroundCallState() + != PreciseCallState.PRECISE_CALL_STATE_ACTIVE) { + mCallNetworkType[phoneId] = TelephonyManager.NETWORK_TYPE_UNKNOWN; + mCallQuality[phoneId] = new CallQuality(); + } + mCallAttributes[phoneId] = new CallAttributes(mPreciseCallState[phoneId], + mCallNetworkType[phoneId], mCallQuality[phoneId]); } - mCallAttributes = new CallAttributes(mPreciseCallState, mCallNetworkType, - mCallQuality); - } - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent(PhoneStateListener.LISTEN_PRECISE_CALL_STATE)) { - try { - r.callback.onPreciseCallStateChanged(mPreciseCallState); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + for (Record r : mRecords) { + if (r.matchPhoneStateListenerEvent(PhoneStateListener.LISTEN_PRECISE_CALL_STATE) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onPreciseCallStateChanged(mPreciseCallState[phoneId]); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } - } - if (notifyCallAttributes && r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_CALL_ATTRIBUTES_CHANGED)) { - try { - r.callback.onCallAttributesChanged(mCallAttributes); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + if (notifyCallAttributes && r.matchPhoneStateListenerEvent( + PhoneStateListener.LISTEN_CALL_ATTRIBUTES_CHANGED) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onCallAttributesChanged(mCallAttributes[phoneId]); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } } } @@ -1610,21 +1650,24 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { backgroundCallState); } - public void notifyDisconnectCause(int disconnectCause, int preciseDisconnectCause) { + public void notifyDisconnectCause(int phoneId, int subId, int disconnectCause, + int preciseDisconnectCause) { if (!checkNotifyPermission("notifyDisconnectCause()")) { return; } synchronized (mRecords) { - mCallDisconnectCause = disconnectCause; - mCallPreciseDisconnectCause = preciseDisconnectCause; - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent(PhoneStateListener - .LISTEN_CALL_DISCONNECT_CAUSES)) { - try { - r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause, - mCallPreciseDisconnectCause); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + if (validatePhoneId(phoneId)) { + mCallDisconnectCause[phoneId] = disconnectCause; + mCallPreciseDisconnectCause[phoneId] = preciseDisconnectCause; + for (Record r : mRecords) { + if (r.matchPhoneStateListenerEvent(PhoneStateListener + .LISTEN_CALL_DISCONNECT_CAUSES) && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onCallDisconnectCauseChanged(mCallDisconnectCause[phoneId], + mCallPreciseDisconnectCause[phoneId]); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } } } @@ -1660,25 +1703,30 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } - public void notifyPreciseDataConnectionFailed(String apnType, + public void notifyPreciseDataConnectionFailed(int phoneId, int subId, String apnType, String apn, @DataFailCause.FailCause int failCause) { if (!checkNotifyPermission("notifyPreciseDataConnectionFailed()")) { return; } synchronized (mRecords) { - mPreciseDataConnectionState = new PreciseDataConnectionState( - TelephonyManager.DATA_UNKNOWN, TelephonyManager.NETWORK_TYPE_UNKNOWN, - ApnSetting.getApnTypesBitmaskFromString(apnType), apn, null, failCause); - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE)) { - try { - r.callback.onPreciseDataConnectionStateChanged(mPreciseDataConnectionState); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + if (validatePhoneId(phoneId)) { + mPreciseDataConnectionState[phoneId] = new PreciseDataConnectionState( + TelephonyManager.DATA_UNKNOWN, TelephonyManager.NETWORK_TYPE_UNKNOWN, + ApnSetting.getApnTypesBitmaskFromString(apnType), apn, null, failCause); + for (Record r : mRecords) { + if (r.matchPhoneStateListenerEvent( + PhoneStateListener.LISTEN_PRECISE_DATA_CONNECTION_STATE) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onPreciseDataConnectionStateChanged( + mPreciseDataConnectionState[phoneId]); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } } } + handleRemoveListLocked(); } broadcastPreciseDataConnectionStateChanged(TelephonyManager.DATA_UNKNOWN, @@ -1716,24 +1764,25 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } - public void notifyOemHookRawEventForSubscriber(int subId, byte[] rawData) { + public void notifyOemHookRawEventForSubscriber(int phoneId, int subId, byte[] rawData) { if (!checkNotifyPermission("notifyOemHookRawEventForSubscriber")) { return; } synchronized (mRecords) { - for (Record r : mRecords) { - if (VDBG) { - log("notifyOemHookRawEventForSubscriber: r=" + r + " subId=" + subId); - } - if ((r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_OEM_HOOK_RAW_EVENT)) && - ((r.subId == subId) || - (r.subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID))) { - try { - r.callback.onOemHookRawEvent(rawData); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + if (validatePhoneId(phoneId)) { + for (Record r : mRecords) { + if (VDBG) { + log("notifyOemHookRawEventForSubscriber: r=" + r + " subId=" + subId); + } + if ((r.matchPhoneStateListenerEvent( + PhoneStateListener.LISTEN_OEM_HOOK_RAW_EVENT)) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onOemHookRawEvent(rawData); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } } } @@ -1804,87 +1853,98 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } - public void notifyRadioPowerStateChanged(@TelephonyManager.RadioPowerState int state) { + public void notifyRadioPowerStateChanged(int phoneId, int subId, + @TelephonyManager.RadioPowerState int state) { if (!checkNotifyPermission("notifyRadioPowerStateChanged()")) { return; } if (VDBG) { - log("notifyRadioPowerStateChanged: state= " + state); + log("notifyRadioPowerStateChanged: state= " + state + " subId=" + subId); } synchronized (mRecords) { - mRadioPowerState = state; + if (validatePhoneId(phoneId)) { + mRadioPowerState = state; - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_RADIO_POWER_STATE_CHANGED)) { - try { - r.callback.onRadioPowerStateChanged(state); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + for (Record r : mRecords) { + if (r.matchPhoneStateListenerEvent( + PhoneStateListener.LISTEN_RADIO_POWER_STATE_CHANGED) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onRadioPowerStateChanged(state); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } } + } handleRemoveListLocked(); } } @Override - public void notifyEmergencyNumberList() { + public void notifyEmergencyNumberList(int phoneId, int subId) { if (!checkNotifyPermission("notifyEmergencyNumberList()")) { return; } synchronized (mRecords) { - TelephonyManager tm = (TelephonyManager) mContext.getSystemService( - Context.TELEPHONY_SERVICE); - mEmergencyNumberList = tm.getEmergencyNumberList(); + if (validatePhoneId(phoneId)) { + TelephonyManager tm = (TelephonyManager) mContext.getSystemService( + Context.TELEPHONY_SERVICE); + mEmergencyNumberList = tm.getEmergencyNumberList(); - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_EMERGENCY_NUMBER_LIST)) { - try { - r.callback.onEmergencyNumberListChanged(mEmergencyNumberList); - if (VDBG) { - log("notifyEmergencyNumberList: emergencyNumberList= " - + mEmergencyNumberList); + for (Record r : mRecords) { + if (r.matchPhoneStateListenerEvent( + PhoneStateListener.LISTEN_EMERGENCY_NUMBER_LIST) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onEmergencyNumberListChanged(mEmergencyNumberList); + if (VDBG) { + log("notifyEmergencyNumberList: emergencyNumberList= " + + mEmergencyNumberList); + } + } catch (RemoteException ex) { + mRemoveList.add(r.binder); } - } catch (RemoteException ex) { - mRemoveList.add(r.binder); } } } + handleRemoveListLocked(); } } @Override - public void notifyCallQualityChanged(CallQuality callQuality, int phoneId, + public void notifyCallQualityChanged(CallQuality callQuality, int phoneId, int subId, int callNetworkType) { if (!checkNotifyPermission("notifyCallQualityChanged()")) { return; } - // merge CallQuality with PreciseCallState and network type - mCallQuality = callQuality; - mCallNetworkType = callNetworkType; - mCallAttributes = new CallAttributes(mPreciseCallState, callNetworkType, callQuality); - synchronized (mRecords) { - TelephonyManager tm = (TelephonyManager) mContext.getSystemService( - Context.TELEPHONY_SERVICE); + if (validatePhoneId(phoneId)) { + // merge CallQuality with PreciseCallState and network type + mCallQuality[phoneId] = callQuality; + mCallNetworkType[phoneId] = callNetworkType; + mCallAttributes[phoneId] = new CallAttributes(mPreciseCallState[phoneId], + callNetworkType, callQuality); - for (Record r : mRecords) { - if (r.matchPhoneStateListenerEvent( - PhoneStateListener.LISTEN_CALL_ATTRIBUTES_CHANGED)) { - try { - r.callback.onCallAttributesChanged(mCallAttributes); - } catch (RemoteException ex) { - mRemoveList.add(r.binder); + for (Record r : mRecords) { + if (r.matchPhoneStateListenerEvent( + PhoneStateListener.LISTEN_CALL_ATTRIBUTES_CHANGED) + && idMatch(r.subId, subId, phoneId)) { + try { + r.callback.onCallAttributesChanged(mCallAttributes[phoneId]); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } } } } + handleRemoveListLocked(); } } @@ -1904,6 +1964,11 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { pw.println("Phone Id=" + i); pw.increaseIndent(); pw.println("mCallState=" + mCallState[i]); + pw.println("mRingingCallState=" + mRingingCallState[i]); + pw.println("mForegroundCallState=" + mForegroundCallState[i]); + pw.println("mBackgroundCallState=" + mBackgroundCallState[i]); + pw.println("mPreciseCallState=" + mPreciseCallState[i]); + pw.println("mCallDisconnectCause=" + mCallDisconnectCause[i]); pw.println("mCallIncomingNumber=" + mCallIncomingNumber[i]); pw.println("mServiceState=" + mServiceState[i]); pw.println("mVoiceActivationState= " + mVoiceActivationState[i]); @@ -1917,26 +1982,23 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { pw.println("mCellLocation=" + mCellLocation[i]); pw.println("mCellInfo=" + mCellInfo.get(i)); pw.println("mImsCallDisconnectCause=" + mImsReasonInfo.get(i)); + pw.println("mSrvccState=" + mSrvccState[i]); + pw.println("mOtaspMode=" + mOtaspMode[i]); + pw.println("mCallPreciseDisconnectCause=" + mCallPreciseDisconnectCause[i]); + pw.println("mCallQuality=" + mCallQuality[i]); + pw.println("mCallAttributes=" + mCallAttributes[i]); + pw.println("mCallNetworkType=" + mCallNetworkType[i]); + pw.println("mPreciseDataConnectionState=" + mPreciseDataConnectionState[i]); pw.decreaseIndent(); } - pw.println("mCallNetworkType=" + mCallNetworkType); - pw.println("mPreciseDataConnectionState=" + mPreciseDataConnectionState); - pw.println("mPreciseCallState=" + mPreciseCallState); - pw.println("mCallDisconnectCause=" + mCallDisconnectCause); - pw.println("mCallPreciseDisconnectCause=" + mCallPreciseDisconnectCause); pw.println("mCarrierNetworkChangeState=" + mCarrierNetworkChangeState); - pw.println("mRingingCallState=" + mRingingCallState); - pw.println("mForegroundCallState=" + mForegroundCallState); - pw.println("mBackgroundCallState=" + mBackgroundCallState); - pw.println("mSrvccState=" + mSrvccState); + pw.println("mPhoneCapability=" + mPhoneCapability); pw.println("mActiveDataSubId=" + mActiveDataSubId); pw.println("mRadioPowerState=" + mRadioPowerState); pw.println("mEmergencyNumberList=" + mEmergencyNumberList); - pw.println("mCallQuality=" + mCallQuality); - pw.println("mCallAttributes=" + mCallAttributes); - pw.println("mDefaultPhoneId" + mDefaultPhoneId); - pw.println("mDefaultSubId" + mDefaultSubId); + pw.println("mDefaultPhoneId=" + mDefaultPhoneId); + pw.println("mDefaultSubId=" + mDefaultSubId); pw.decreaseIndent(); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index d5109123ab12..ad5f4e6e33c2 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -36,6 +36,8 @@ import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.IUidObserver; import android.app.NotificationManager; +import android.app.role.OnRoleHoldersChangedListener; +import android.app.role.RoleManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; @@ -158,6 +160,7 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.concurrent.Executor; /** * The implementation of the volume manager service. @@ -889,9 +892,48 @@ public class AudioService extends IAudioService.Stub 0 : SAFE_VOLUME_CONFIGURE_TIMEOUT_MS); initA11yMonitoring(); + + mRoleObserver = new RoleObserver(); + mRoleObserver.register(); + onIndicateSystemReady(); } + RoleObserver mRoleObserver; + + class RoleObserver implements OnRoleHoldersChangedListener { + private RoleManager mRm; + private final Executor mExecutor; + + RoleObserver() { + mExecutor = mContext.getMainExecutor(); + } + + public void register() { + mRm = (RoleManager) mContext.getSystemService(Context.ROLE_SERVICE); + if (mRm != null) { + mRm.addOnRoleHoldersChangedListenerAsUser(mExecutor, this, UserHandle.ALL); + updateAssistantUId(true); + } + } + + @Override + public void onRoleHoldersChanged(@NonNull String roleName, @NonNull UserHandle user) { + if (RoleManager.ROLE_ASSISTANT.equals(roleName)) { + updateAssistantUId(false); + } + } + + public String getAssistantRoleHolder() { + String assitantPackage = ""; + if (mRm != null) { + List<String> assistants = mRm.getRoleHolders(RoleManager.ROLE_ASSISTANT); + assitantPackage = assistants.size() == 0 ? "" : assistants.get(0); + } + return assitantPackage; + } + } + void onIndicateSystemReady() { if (AudioSystem.systemReady() == AudioSystem.SUCCESS) { return; @@ -1391,21 +1433,33 @@ public class AudioService extends IAudioService.Stub int assistantUid = 0; // Consider assistants in the following order of priority: - // 1) voice interaction service - // 2) assistant - String assistantName = Settings.Secure.getStringForUser( + // 1) apk in assistant role + // 2) voice interaction service + // 3) assistant service + + String packageName = ""; + if (mRoleObserver != null) { + packageName = mRoleObserver.getAssistantRoleHolder(); + } + if (TextUtils.isEmpty(packageName)) { + String assistantName = Settings.Secure.getStringForUser( + mContentResolver, + Settings.Secure.VOICE_INTERACTION_SERVICE, UserHandle.USER_CURRENT); + if (TextUtils.isEmpty(assistantName)) { + assistantName = Settings.Secure.getStringForUser( mContentResolver, - Settings.Secure.VOICE_INTERACTION_SERVICE, UserHandle.USER_CURRENT); - if (TextUtils.isEmpty(assistantName)) { - assistantName = Settings.Secure.getStringForUser( - mContentResolver, - Settings.Secure.ASSISTANT, UserHandle.USER_CURRENT); - } - if (!TextUtils.isEmpty(assistantName)) { - String packageName = ComponentName.unflattenFromString(assistantName).getPackageName(); - if (!TextUtils.isEmpty(packageName)) { + Settings.Secure.ASSISTANT, UserHandle.USER_CURRENT); + } + if (!TextUtils.isEmpty(assistantName)) { + packageName = ComponentName.unflattenFromString(assistantName).getPackageName(); + } + } + if (!TextUtils.isEmpty(packageName)) { + PackageManager pm = mContext.getPackageManager(); + if (pm.checkPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD, packageName) + == PackageManager.PERMISSION_GRANTED) { try { - assistantUid = mContext.getPackageManager().getPackageUid(packageName, 0); + assistantUid = pm.getPackageUid(packageName, 0); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "updateAssistantUId() could not find UID for package: " + packageName); diff --git a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java index 9fce644d6c4b..171cc5abdb97 100644 --- a/services/core/java/com/android/server/display/BrightnessMappingStrategy.java +++ b/services/core/java/com/android/server/display/BrightnessMappingStrategy.java @@ -778,6 +778,7 @@ public abstract class BrightnessMappingStrategy { pw.println(" mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment); pw.println(" mUserLux=" + mUserLux); pw.println(" mUserBrightness=" + mUserBrightness); + pw.println(" mDefaultConfig=" + mDefaultConfig); } private void computeSpline() { diff --git a/services/core/java/com/android/server/display/color/DisplayTransformManager.java b/services/core/java/com/android/server/display/color/DisplayTransformManager.java index d6aa2ba02f1f..5ff45a97706e 100644 --- a/services/core/java/com/android/server/display/color/DisplayTransformManager.java +++ b/services/core/java/com/android/server/display/color/DisplayTransformManager.java @@ -89,12 +89,6 @@ public class DisplayTransformManager { private static final int DISPLAY_COLOR_MANAGED = 0; private static final int DISPLAY_COLOR_UNMANAGED = 1; private static final int DISPLAY_COLOR_ENHANCED = 2; - /** - * Display color mode range reserved for vendor customizations by the RenderIntent definition in - * hardware/interfaces/graphics/common/1.1/types.hal. - */ - private static final int VENDOR_MODE_RANGE_MIN = 256; // 0x100 - private static final int VENDOR_MODE_RANGE_MAX = 511; // 0x1ff /** * Map of level -> color transformation matrix. @@ -270,7 +264,8 @@ public class DisplayTransformManager { } else if (colorMode == ColorDisplayManager.COLOR_MODE_AUTOMATIC) { applySaturation(COLOR_SATURATION_NATURAL); setDisplayColor(DISPLAY_COLOR_ENHANCED); - } else if (colorMode >= VENDOR_MODE_RANGE_MIN && colorMode <= VENDOR_MODE_RANGE_MAX) { + } else if (colorMode >= ColorDisplayManager.VENDOR_COLOR_MODE_RANGE_MIN + && colorMode <= ColorDisplayManager.VENDOR_COLOR_MODE_RANGE_MAX) { applySaturation(COLOR_SATURATION_NATURAL); setDisplayColor(colorMode); } diff --git a/services/core/java/com/android/server/location/GnssLocationProvider.java b/services/core/java/com/android/server/location/GnssLocationProvider.java index 5f1f20294bc1..7ab46f60cf90 100644 --- a/services/core/java/com/android/server/location/GnssLocationProvider.java +++ b/services/core/java/com/android/server/location/GnssLocationProvider.java @@ -2055,7 +2055,7 @@ public class GnssLocationProvider extends AbstractLocationProvider implements setupNativeGnssService(/* reinitializeGnssServiceHandle = */ false); if (native_is_gnss_visibility_control_supported()) { - mGnssVisibilityControl = new GnssVisibilityControl(mContext, mLooper); + mGnssVisibilityControl = new GnssVisibilityControl(mContext, mLooper, mNIHandler); } // load default GPS configuration diff --git a/services/core/java/com/android/server/location/GnssVisibilityControl.java b/services/core/java/com/android/server/location/GnssVisibilityControl.java index c3626d2373ea..860138675529 100644 --- a/services/core/java/com/android/server/location/GnssVisibilityControl.java +++ b/services/core/java/com/android/server/location/GnssVisibilityControl.java @@ -33,6 +33,9 @@ import android.util.ArrayMap; import android.util.Log; import android.util.StatsLog; +import com.android.internal.R; +import com.android.internal.location.GpsNetInitiatedHandler; + import java.util.Arrays; import java.util.List; import java.util.Map; @@ -65,6 +68,7 @@ class GnssVisibilityControl { private final Handler mHandler; private final Context mContext; + private final GpsNetInitiatedHandler mNiHandler; private boolean mIsGpsEnabled; @@ -76,11 +80,12 @@ class GnssVisibilityControl { private PackageManager.OnPermissionsChangedListener mOnPermissionsChangedListener = uid -> runOnHandler(() -> handlePermissionsChanged(uid)); - GnssVisibilityControl(Context context, Looper looper) { + GnssVisibilityControl(Context context, Looper looper, GpsNetInitiatedHandler niHandler) { mContext = context; PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_KEY); mHandler = new Handler(looper); + mNiHandler = niHandler; mAppOps = mContext.getSystemService(AppOpsManager.class); mPackageManager = mContext.getPackageManager(); @@ -250,6 +255,9 @@ class GnssVisibilityControl { private static final byte NFW_RESPONSE_TYPE_ACCEPTED_NO_LOCATION_PROVIDED = 1; private static final byte NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED = 2; + // This must match with NfwProtocolStack enum in IGnssVisibilityControlCallback.hal. + private static final byte NFW_PROTOCOL_STACK_SUPL = 1; + private final String mProxyAppPackageName; private final byte mProtocolStack; private final String mOtherProtocolStackName; @@ -299,6 +307,10 @@ class GnssVisibilityControl { return mResponseType != NfwNotification.NFW_RESPONSE_TYPE_REJECTED; } + private boolean isLocationProvided() { + return mResponseType == NfwNotification.NFW_RESPONSE_TYPE_ACCEPTED_LOCATION_PROVIDED; + } + private boolean isRequestAttributedToProxyApp() { return !TextUtils.isEmpty(mProxyAppPackageName); } @@ -306,6 +318,10 @@ class GnssVisibilityControl { private boolean isEmergencyRequestNotification() { return mInEmergencyMode && !isRequestAttributedToProxyApp(); } + + private boolean isRequestTypeSupl() { + return mProtocolStack == NFW_PROTOCOL_STACK_SUPL; + } } private void handlePermissionsChanged(int uid) { @@ -430,16 +446,15 @@ class GnssVisibilityControl { return; } - Log.e(TAG, "ProxyAppPackageName field is not set. AppOps service not notified " - + "for non-framework location access notification: " + nfwNotification); + Log.e(TAG, "ProxyAppPackageName field is not set. AppOps service not notified" + + " for notification: " + nfwNotification); return; } if (isLocationPermissionEnabled == null) { - Log.w(TAG, "Could not find proxy app with name: " + proxyAppPkgName + " in the " - + "value specified for config parameter: " - + GnssConfiguration.CONFIG_NFW_PROXY_APPS + ". AppOps service not notified " - + "for non-framework location access notification: " + nfwNotification); + Log.w(TAG, "Could not find proxy app " + proxyAppPkgName + " in the value specified for" + + " config parameter: " + GnssConfiguration.CONFIG_NFW_PROXY_APPS + + ". AppOps service not notified for notification: " + nfwNotification); return; } @@ -447,8 +462,7 @@ class GnssVisibilityControl { final ApplicationInfo proxyAppInfo = getProxyAppInfo(proxyAppPkgName); if (proxyAppInfo == null) { Log.e(TAG, "Proxy app " + proxyAppPkgName + " is not found. AppOps service not " - + "notified for non-framework location access notification: " - + nfwNotification); + + "notified for notification: " + nfwNotification); return; } @@ -465,13 +479,40 @@ class GnssVisibilityControl { } private void handleEmergencyNfwNotification(NfwNotification nfwNotification) { - boolean isPermissionMismatched = - (nfwNotification.mResponseType == NfwNotification.NFW_RESPONSE_TYPE_REJECTED); - if (isPermissionMismatched) { + boolean isPermissionMismatched = false; + if (!nfwNotification.isRequestAccepted()) { Log.e(TAG, "Emergency non-framework location request incorrectly rejected." + " Notification: " + nfwNotification); + isPermissionMismatched = true; + } + + if (!mNiHandler.getInEmergency()) { + Log.w(TAG, "Emergency state mismatch. Device currently not in user initiated emergency" + + " session. Notification: " + nfwNotification); + isPermissionMismatched = true; } + logEvent(nfwNotification, isPermissionMismatched); + + if (nfwNotification.isLocationProvided()) { + // Emulate deprecated IGnssNi.hal user notification of emergency NI requests. + GpsNetInitiatedHandler.GpsNiNotification notification = + new GpsNetInitiatedHandler.GpsNiNotification(); + notification.notificationId = 0; + notification.niType = nfwNotification.isRequestTypeSupl() + ? GpsNetInitiatedHandler.GPS_NI_TYPE_EMERGENCY_SUPL + : GpsNetInitiatedHandler.GPS_NI_TYPE_UMTS_CTRL_PLANE; + notification.needNotify = true; + notification.needVerify = false; + notification.privacyOverride = false; + notification.timeout = 0; + notification.defaultResponse = GpsNetInitiatedHandler.GPS_NI_RESPONSE_NORESP; + notification.requestorId = nfwNotification.mRequestorId; + notification.requestorIdEncoding = GpsNetInitiatedHandler.GPS_ENC_NONE; + notification.text = mContext.getString(R.string.global_action_emergency); + notification.textEncoding = GpsNetInitiatedHandler.GPS_ENC_NONE; + mNiHandler.setNiNotification(notification); + } } private void logEvent(NfwNotification notification, boolean isPermissionMismatched) { diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java index 497385fef39c..5430f4c8daa0 100644 --- a/services/core/java/com/android/server/pm/ApexManager.java +++ b/services/core/java/com/android/server/pm/ApexManager.java @@ -22,21 +22,22 @@ import android.apex.ApexInfo; import android.apex.ApexInfoList; import android.apex.ApexSessionInfo; import android.apex.IApexService; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageParser; import android.content.pm.PackageParser.PackageParserException; -import android.os.HandlerThread; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; -import android.os.SystemClock; import android.sysprop.ApexProperties; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.IndentingPrintWriter; -import com.android.server.SystemService; import java.io.File; import java.io.PrintWriter; @@ -45,108 +46,75 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.CountDownLatch; import java.util.function.Function; import java.util.stream.Collectors; /** * ApexManager class handles communications with the apex service to perform operation and queries, * as well as providing caching to avoid unnecessary calls to the service. - * - * @hide */ -public final class ApexManager extends SystemService { - private static final String TAG = "ApexManager"; - private IApexService mApexService; - - private final CountDownLatch mActivePackagesCacheLatch = new CountDownLatch(1); +class ApexManager { + static final String TAG = "ApexManager"; + private final IApexService mApexService; + private final Context mContext; + private final Object mLock = new Object(); + @GuardedBy("mLock") private Map<String, PackageInfo> mActivePackagesCache; - private final CountDownLatch mApexFilesCacheLatch = new CountDownLatch(1); - private ApexInfo[] mApexFiles; - - public ApexManager(Context context) { - super(context); - } - - @Override - public void onStart() { + ApexManager(Context context) { try { mApexService = IApexService.Stub.asInterface( - ServiceManager.getServiceOrThrow("apexservice")); + ServiceManager.getServiceOrThrow("apexservice")); } catch (ServiceNotFoundException e) { throw new IllegalStateException("Required service apexservice not available"); } - publishLocalService(ApexManager.class, this); - HandlerThread oneShotThread = new HandlerThread("ApexManagerOneShotHandler"); - oneShotThread.start(); - oneShotThread.getThreadHandler().post(this::initSequence); - oneShotThread.quitSafely(); - } - - private void initSequence() { - populateApexFilesCache(); - parseApexFiles(); + mContext = context; } - private void populateApexFilesCache() { - if (mApexFiles != null) { - return; - } - long startTimeMicros = SystemClock.currentTimeMicro(); - Slog.i(TAG, "Starting to populate apex files cache"); - try { - mApexFiles = mApexService.getActivePackages(); - Slog.i(TAG, "IPC to apexd finished in " + (SystemClock.currentTimeMicro() - - startTimeMicros) + " μs"); - } catch (RemoteException re) { - // TODO: make sure this error is propagated to system server. - Slog.e(TAG, "Unable to retrieve packages from apexservice: " + re.toString()); - re.rethrowAsRuntimeException(); - } - mApexFilesCacheLatch.countDown(); - Slog.i(TAG, "Finished populating apex files cache in " + (SystemClock.currentTimeMicro() - - startTimeMicros) + " μs"); + void systemReady() { + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + onBootCompleted(); + mContext.unregisterReceiver(this); + } + }, new IntentFilter(Intent.ACTION_BOOT_COMPLETED)); } - private void parseApexFiles() { - waitForLatch(mApexFilesCacheLatch); - if (mApexFiles == null) { - throw new IllegalStateException("mApexFiles must be populated"); - } - long startTimeMicros = SystemClock.currentTimeMicro(); - Slog.i(TAG, "Starting to parse apex files"); - List<PackageInfo> list = new ArrayList<>(); - // TODO: this can be parallelized. - for (ApexInfo ai : mApexFiles) { + private void populateActivePackagesCacheIfNeeded() { + synchronized (mLock) { + if (mActivePackagesCache != null) { + return; + } try { - // If the device is using flattened APEX, don't report any APEX - // packages since they won't be managed or updated by PackageManager. - if ((new File(ai.packagePath)).isDirectory()) { - break; - } - list.add(PackageParser.generatePackageInfoFromApex( - new File(ai.packagePath), PackageManager.GET_META_DATA + List<PackageInfo> list = new ArrayList<>(); + final ApexInfo[] activePkgs = mApexService.getActivePackages(); + for (ApexInfo ai : activePkgs) { + // If the device is using flattened APEX, don't report any APEX + // packages since they won't be managed or updated by PackageManager. + if ((new File(ai.packagePath)).isDirectory()) { + break; + } + try { + list.add(PackageParser.generatePackageInfoFromApex( + new File(ai.packagePath), PackageManager.GET_META_DATA | PackageManager.GET_SIGNING_CERTIFICATES)); - } catch (PackageParserException pe) { - // TODO: make sure this error is propagated to system server. - throw new IllegalStateException("Unable to parse: " + ai, pe); + } catch (PackageParserException pe) { + throw new IllegalStateException("Unable to parse: " + ai, pe); + } + } + mActivePackagesCache = list.stream().collect( + Collectors.toMap(p -> p.packageName, Function.identity())); + } catch (RemoteException re) { + Slog.e(TAG, "Unable to retrieve packages from apexservice: " + re.toString()); + throw new RuntimeException(re); } } - mActivePackagesCache = list.stream().collect( - Collectors.toMap(p -> p.packageName, Function.identity())); - mActivePackagesCacheLatch.countDown(); - Slog.i(TAG, "Finished parsing apex files in " + (SystemClock.currentTimeMicro() - - startTimeMicros) + " μs"); } /** * Retrieves information about an active APEX package. * - * <p>This method blocks caller thread until {@link #parseApexFiles()} succeeds. Note that in - * case {@link #parseApexFiles()}} throws an exception this method will never finish - * essentially putting device into a boot loop. - * * @param packageName the package name to look for. Note that this is the package name reported * in the APK container manifest (i.e. AndroidManifest.xml), which might * differ from the one reported in the APEX manifest (i.e. @@ -155,43 +123,30 @@ public final class ApexManager extends SystemService { * is not found. */ @Nullable PackageInfo getActivePackage(String packageName) { - waitForLatch(mActivePackagesCacheLatch); + populateActivePackagesCacheIfNeeded(); return mActivePackagesCache.get(packageName); } /** * Retrieves information about all active APEX packages. * - * <p>This method blocks caller thread until {@link #parseApexFiles()} succeeds. Note that in - * case {@link #parseApexFiles()}} throws an exception this method will never finish - * essentially putting device into a boot loop. - * * @return a Collection of PackageInfo object, each one containing information about a different * active package. */ Collection<PackageInfo> getActivePackages() { - waitForLatch(mActivePackagesCacheLatch); + populateActivePackagesCacheIfNeeded(); return mActivePackagesCache.values(); } /** * Checks if {@code packageName} is an apex package. * - * <p>This method blocks caller thread until {@link #populateApexFilesCache()} succeeds. Note - * that in case {@link #populateApexFilesCache()} throws an exception this method will never - * finish essentially putting device into a boot loop. - * * @param packageName package to check. * @return {@code true} if {@code packageName} is an apex package. */ boolean isApexPackage(String packageName) { - waitForLatch(mApexFilesCacheLatch); - for (ApexInfo ai : mApexFiles) { - if (ai.packageName.equals(packageName)) { - return true; - } - } - return false; + populateActivePackagesCacheIfNeeded(); + return mActivePackagesCache.containsKey(packageName); } /** @@ -319,19 +274,6 @@ public final class ApexManager extends SystemService { } /** - * Blocks current thread until {@code latch} has counted down to zero. - * - * @throws RuntimeException if thread was interrupted while waiting. - */ - private void waitForLatch(CountDownLatch latch) { - try { - latch.await(); - } catch (InterruptedException e) { - throw new RuntimeException("Interrupted waiting for cache to be populated", e); - } - } - - /** * Dumps various state information to the provided {@link PrintWriter} object. * * @param pw the {@link PrintWriter} object to send information to. @@ -344,7 +286,7 @@ public final class ApexManager extends SystemService { ipw.println("Active APEX packages:"); ipw.increaseIndent(); try { - waitForLatch(mActivePackagesCacheLatch); + populateActivePackagesCacheIfNeeded(); for (PackageInfo pi : mActivePackagesCache.values()) { if (packageName != null && !packageName.equals(pi.packageName)) { continue; @@ -389,4 +331,8 @@ public final class ApexManager extends SystemService { ipw.println("Couldn't communicate with apexd."); } } + + public void onBootCompleted() { + populateActivePackagesCacheIfNeeded(); + } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index e6313d9dcbe4..44f76774fc8a 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -363,8 +363,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements System.currentTimeMillis() - session.getUpdatedMillis(); final boolean valid; if (session.isStaged()) { - if (timeSinceUpdate >= MAX_TIME_SINCE_UPDATE_MILLIS - && session.isStagedAndInTerminalState()) { + if (timeSinceUpdate >= MAX_TIME_SINCE_UPDATE_MILLIS) { valid = false; } else { valid = true; @@ -531,16 +530,6 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements + "to use the PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS flag"); } - // Only system components can circumvent restricted whitelisting when installing. - if ((params.installFlags - & PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS) != 0 - && mContext.checkCallingOrSelfPermission(Manifest.permission - .WHITELIST_RESTRICTED_PERMISSIONS) == PackageManager.PERMISSION_DENIED) { - throw new SecurityException("You need the " - + "android.permission.WHITELIST_RESTRICTED_PERMISSIONS permission to" - + " use the PackageManager.INSTALL_WHITELIST_RESTRICTED_PERMISSIONS flag"); - } - // Defensively resize giant app icons if (params.appIcon != null) { final ActivityManager am = (ActivityManager) mContext.getSystemService( diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 5f6e7399f3a7..6f9a918d105c 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -1150,7 +1150,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { */ void sealAndValidateIfNecessary() { synchronized (mLock) { - if (!mShouldBeSealed) { + if (!mShouldBeSealed || isStagedAndInTerminalState()) { return; } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index e935771aea63..95a88092fd6e 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -2375,8 +2375,6 @@ public class PackageManagerService extends IPackageManager.Stub public PackageManagerService(Context context, Installer installer, boolean factoryTest, boolean onlyCore) { - mApexManager = LocalServices.getService(ApexManager.class); - LockGuard.installLock(mPackages, LockGuard.INDEX_PACKAGES); Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "create package manager"); EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_START, @@ -2473,6 +2471,7 @@ public class PackageManagerService extends IPackageManager.Stub mProtectedPackages = new ProtectedPackages(mContext); + mApexManager = new ApexManager(context); synchronized (mInstallLock) { // writer synchronized (mPackages) { @@ -11726,7 +11725,12 @@ public class PackageManagerService extends IPackageManager.Stub "Code and resource paths haven't been set correctly"); } - if (mApexManager.isApexPackage(pkg.packageName)) { + // Check that there is an APEX package with the same name only during install/first boot + // after OTA. + final boolean isUserInstall = (scanFlags & SCAN_BOOTING) == 0; + final boolean isFirstBootOrUpgrade = (scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) != 0; + if ((isUserInstall || isFirstBootOrUpgrade) + && mApexManager.isApexPackage(pkg.packageName)) { throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE, pkg.packageName + " is an APEX package and can't be installed as an APK."); } @@ -21526,6 +21530,7 @@ public class PackageManagerService extends IPackageManager.Stub storage.registerListener(mStorageListener); mInstallerService.systemReady(); + mApexManager.systemReady(); mPackageDexOptimizer.systemReady(); getStorageManagerInternal().addExternalStoragePolicy( diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 33dd48a1ac6a..fbf074e3ba15 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -2351,9 +2351,10 @@ class PackageManagerShellCommand extends ShellCommand { break; case "-g": sessionParams.installFlags |= PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS; - case "-w": - sessionParams.installFlags |= - PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS; + break; + case "--restrict-permissions": + sessionParams.installFlags &= + ~PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS; break; case "--dont-kill": sessionParams.installFlags |= PackageManager.INSTALL_DONT_KILL_APP; @@ -3004,10 +3005,10 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" -d: allow version code downgrade (debuggable packages only)"); pw.println(" -p: partial application install (new split on top of existing pkg)"); pw.println(" -g: grant all runtime permissions"); - pw.println(" -w: whitelist all restricted permissions"); pw.println(" -S: size in bytes of package, required for stdin"); pw.println(" --user: install under the given user."); pw.println(" --dont-kill: installing a new feature split, don't kill running app"); + pw.println(" --restrict-permissions: don't whitelist restricted permissions at install"); pw.println(" --originating-uri: set URI where app was downloaded from"); pw.println(" --referrer: set URI that instigated the install of the app"); pw.println(" --pkg: specify expected package name of app being installed"); diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java index e0256460042a..4fdf1bc58e05 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -1134,7 +1134,7 @@ public final class DefaultPermissionGrantPolicy { private void grantRuntimePermissions(PackageInfo pkg, Set<String> permissionsWithoutSplits, boolean systemFixed, boolean ignoreSystemPackage, boolean whitelistRestrictedPermissions, int userId) { - UserHandle user = UserHandle.of(userId); + UserHandle user = UserHandle.of(userId); if (pkg == null) { return; } @@ -1203,7 +1203,7 @@ public final class DefaultPermissionGrantPolicy { if (ArrayUtils.isEmpty(disabledPkg.requestedPermissions)) { return; } - if (!requestedPermissions.equals(disabledPkg.requestedPermissions)) { + if (!Arrays.equals(requestedPermissions, disabledPkg.requestedPermissions)) { grantablePermissions = new ArraySet<>(Arrays.asList(requestedPermissions)); requestedPermissions = disabledPkg.requestedPermissions; } @@ -1213,7 +1213,7 @@ public final class DefaultPermissionGrantPolicy { final int numRequestedPermissions = requestedPermissions.length; // Sort requested permissions so that all permissions that are a foreground permission (i.e. - // permisions that have background permission) are before their background permissions. + // permissions that have a background permission) are before their background permissions. final String[] sortedRequestedPermissions = new String[numRequestedPermissions]; int numForeground = 0; int numOther = 0; @@ -1258,9 +1258,16 @@ public final class DefaultPermissionGrantPolicy { continue; } - int uid = UserHandle.getUid(userId, - UserHandle.getAppId(pkg.applicationInfo.uid)); - String op = AppOpsManager.permissionToOp(permission); + // Preserve whitelisting flags. + newFlags |= (flags & PackageManager.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT); + + // If we are whitelisting the permission, update the exempt flag before grant. + if (whitelistRestrictedPermissions && isPermissionRestricted(permission)) { + mContext.getPackageManager().updatePermissionFlags(permission, + pkg.packageName, + PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT, + PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT, user); + } if (pm.checkPermission(permission, pkg.packageName) != PackageManager.PERMISSION_GRANTED) { @@ -1268,13 +1275,12 @@ public final class DefaultPermissionGrantPolicy { .grantRuntimePermission(pkg.packageName, permission, user); } - if (whitelistRestrictedPermissions && isPermissionRestricted(permission)) { - newFlags |= PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT; - } - mContext.getPackageManager().updatePermissionFlags(permission, pkg.packageName, newFlags, newFlags, user); + int uid = UserHandle.getUid(userId, + UserHandle.getAppId(pkg.applicationInfo.uid)); + List<String> fgPerms = mPermissionManager.getBackgroundPermissions() .get(permission); if (fgPerms != null) { @@ -1285,6 +1291,7 @@ public final class DefaultPermissionGrantPolicy { if (pm.checkPermission(fgPerm, pkg.packageName) == PackageManager.PERMISSION_GRANTED) { // Upgrade the app-op state of the fg permission to allow bg access + // TODO: Dont' call app ops from package manager code. mContext.getSystemService(AppOpsManager.class).setUidMode( AppOpsManager.permissionToOp(fgPerm), uid, AppOpsManager.MODE_ALLOWED); @@ -1295,8 +1302,10 @@ public final class DefaultPermissionGrantPolicy { } String bgPerm = getBackgroundPermission(permission); + String op = AppOpsManager.permissionToOp(permission); if (bgPerm == null) { if (op != null) { + // TODO: Dont' call app ops from package manager code. mContext.getSystemService(AppOpsManager.class).setUidMode(op, uid, AppOpsManager.MODE_ALLOWED); } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index d45a8ef4e0ae..37c1aaa45b8b 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -32,7 +32,6 @@ import static android.content.pm.PackageManager.FLAG_PERMISSION_WHITELIST_INSTAL import static android.content.pm.PackageManager.FLAG_PERMISSION_WHITELIST_SYSTEM; import static android.content.pm.PackageManager.FLAG_PERMISSION_WHITELIST_UPGRADE; import static android.content.pm.PackageManager.MASK_PERMISSION_FLAGS_ALL; -import static android.content.pm.PackageManager.RESTRICTED_PERMISSIONS_ENABLED; import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER; import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL; @@ -1058,8 +1057,8 @@ public class PermissionManagerService { boolean wasChanged = false; - boolean restrictionExempt = !RESTRICTED_PERMISSIONS_ENABLED - || (origPermissions.getPermissionFlags(bp.name, userId) + boolean restrictionExempt = + (origPermissions.getPermissionFlags(bp.name, userId) & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0; boolean restrictionApplied = (origPermissions.getPermissionFlags( bp.name, userId) & FLAG_PERMISSION_APPLY_RESTRICTION) != 0; @@ -1177,8 +1176,8 @@ public class PermissionManagerService { for (int userId : currentUserIds) { boolean wasChanged = false; - boolean restrictionExempt = !RESTRICTED_PERMISSIONS_ENABLED - || (origPermissions.getPermissionFlags(bp.name, userId) + boolean restrictionExempt = + (origPermissions.getPermissionFlags(bp.name, userId) & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0; boolean restrictionApplied = (origPermissions.getPermissionFlags( bp.name, userId) & FLAG_PERMISSION_APPLY_RESTRICTION) != 0; @@ -2054,7 +2053,7 @@ public class PermissionManagerService { return; } - if (RESTRICTED_PERMISSIONS_ENABLED && bp.isHardOrSoftRestricted() + if (bp.isHardOrSoftRestricted() && (flags & PackageManager.FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) == 0) { Log.e(TAG, "Cannot grant restricted non-exempt permission " + permName + " for package " + packageName); diff --git a/services/core/java/com/android/server/policy/PermissionPolicyService.java b/services/core/java/com/android/server/policy/PermissionPolicyService.java index a280d83fac27..250f3313e3c4 100644 --- a/services/core/java/com/android/server/policy/PermissionPolicyService.java +++ b/services/core/java/com/android/server/policy/PermissionPolicyService.java @@ -326,8 +326,8 @@ public final class PermissionPolicyService extends SystemService { return; } - final boolean applyRestriction = PackageManager.RESTRICTED_PERMISSIONS_ENABLED - && (mPackageManager.getPermissionFlags(permission, pkg.packageName, + final boolean applyRestriction = + (mPackageManager.getPermissionFlags(permission, pkg.packageName, mContext.getUser()) & FLAG_PERMISSION_APPLY_RESTRICTION) != 0; if (permissionInfo.isHardRestricted()) { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 7a26f7cbf640..d847af0e9b56 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -78,6 +78,8 @@ import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_W import static com.android.server.wm.DisplayContentProto.ABOVE_APP_WINDOWS; import static com.android.server.wm.DisplayContentProto.APP_TRANSITION; import static com.android.server.wm.DisplayContentProto.BELOW_APP_WINDOWS; +import static com.android.server.wm.DisplayContentProto.CHANGING_APPS; +import static com.android.server.wm.DisplayContentProto.CLOSING_APPS; import static com.android.server.wm.DisplayContentProto.DISPLAY_FRAMES; import static com.android.server.wm.DisplayContentProto.DISPLAY_INFO; import static com.android.server.wm.DisplayContentProto.DOCKED_STACK_DIVIDER_CONTROLLER; @@ -85,14 +87,12 @@ import static com.android.server.wm.DisplayContentProto.DPI; import static com.android.server.wm.DisplayContentProto.FOCUSED_APP; import static com.android.server.wm.DisplayContentProto.ID; import static com.android.server.wm.DisplayContentProto.IME_WINDOWS; +import static com.android.server.wm.DisplayContentProto.OPENING_APPS; import static com.android.server.wm.DisplayContentProto.PINNED_STACK_CONTROLLER; import static com.android.server.wm.DisplayContentProto.ROTATION; import static com.android.server.wm.DisplayContentProto.SCREEN_ROTATION_ANIMATION; import static com.android.server.wm.DisplayContentProto.STACKS; import static com.android.server.wm.DisplayContentProto.WINDOW_CONTAINER; -import static com.android.server.wm.DisplayContentProto.OPENING_APPS; -import static com.android.server.wm.DisplayContentProto.CHANGING_APPS; -import static com.android.server.wm.DisplayContentProto.CLOSING_APPS; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_BOOT; @@ -2945,9 +2945,16 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo forAllWindows(mScheduleToastTimeout, false /* traverseTopToBottom */); } - WindowState findFocusedWindowIfNeeded() { - return (mWmService.mPerDisplayFocusEnabled - || mWmService.mRoot.mTopFocusedAppByProcess.isEmpty()) ? findFocusedWindow() : null; + /** + * Looking for the focused window on this display if the top focused display hasn't been + * found yet (topFocusedDisplayId is INVALID_DISPLAY) or per-display focused was allowed. + * + * @param topFocusedDisplayId Id of the top focused display. + * @return The focused window or null if there isn't any or no need to seek. + */ + WindowState findFocusedWindowIfNeeded(int topFocusedDisplayId) { + return (mWmService.mPerDisplayFocusEnabled || topFocusedDisplayId == INVALID_DISPLAY) + ? findFocusedWindow() : null; } WindowState findFocusedWindow() { @@ -2971,10 +2978,12 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo * {@link WindowManagerService#UPDATE_FOCUS_WILL_PLACE_SURFACES}, * {@link WindowManagerService#UPDATE_FOCUS_REMOVING_FOCUS} * @param updateInputWindows Whether to sync the window information to the input module. + * @param topFocusedDisplayId Display id of current top focused display. * @return {@code true} if the focused window has changed. */ - boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows) { - WindowState newFocus = findFocusedWindowIfNeeded(); + boolean updateFocusedWindowLocked(int mode, boolean updateInputWindows, + int topFocusedDisplayId) { + WindowState newFocus = findFocusedWindowIfNeeded(topFocusedDisplayId); if (mCurrentFocus == newFocus) { return false; } @@ -2994,7 +3003,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo if (imWindowChanged) { mWmService.mWindowsChanged = true; setLayoutNeeded(); - newFocus = findFocusedWindowIfNeeded(); + newFocus = findFocusedWindowIfNeeded(topFocusedDisplayId); } if (mCurrentFocus != newFocus) { mWmService.mH.obtainMessage(REPORT_FOCUS_CHANGE, this).sendToTarget(); diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 9f4232400221..8a5f52f8c453 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -170,7 +170,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> int topFocusedDisplayId = INVALID_DISPLAY; for (int i = mChildren.size() - 1; i >= 0; --i) { final DisplayContent dc = mChildren.get(i); - changed |= dc.updateFocusedWindowLocked(mode, updateInputWindows); + changed |= dc.updateFocusedWindowLocked(mode, updateInputWindows, topFocusedDisplayId); final WindowState newFocus = dc.mCurrentFocus; if (newFocus != null) { final int pidOfNewFocus = newFocus.mSession.mPid; @@ -180,6 +180,11 @@ class RootWindowContainer extends WindowContainer<DisplayContent> if (topFocusedDisplayId == INVALID_DISPLAY) { topFocusedDisplayId = dc.getDisplayId(); } + } else if (topFocusedDisplayId == INVALID_DISPLAY && dc.mFocusedApp != null) { + // The top-most display that has a focused app should still be the top focused + // display even when the app window is not ready yet (process not attached or + // window not added yet). + topFocusedDisplayId = dc.getDisplayId(); } } if (topFocusedDisplayId == INVALID_DISPLAY) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 4ac8342e6e60..be7dd31380ba 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -115,7 +115,6 @@ import com.android.server.om.OverlayManagerService; import com.android.server.os.BugreportManagerService; import com.android.server.os.DeviceIdentifiersPolicyService; import com.android.server.os.SchedulingPolicyService; -import com.android.server.pm.ApexManager; import com.android.server.pm.BackgroundDexOptService; import com.android.server.pm.CrossProfileAppsService; import com.android.server.pm.DynamicCodeLoggingService; @@ -628,12 +627,6 @@ public final class SystemServer { watchdog.start(); traceEnd(); - // Start ApexManager as early as we can to give it enough time to call apexd and populate - // cache of known apex packages. Note that calling apexd will happen asynchronously. - traceBeginAndSlog("StartApexManager"); - mSystemServiceManager.startService(ApexManager.class); - traceEnd(); - Slog.i(TAG, "Reading configuration..."); final String TAG_SYSTEM_CONFIG = "ReadingSystemConfig"; traceBeginAndSlog(TAG_SYSTEM_CONFIG); diff --git a/services/net/java/android/net/ip/IpClientManager.java b/services/net/java/android/net/ip/IpClientManager.java new file mode 100644 index 000000000000..f8d7e845da2c --- /dev/null +++ b/services/net/java/android/net/ip/IpClientManager.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.ip; + +import android.annotation.NonNull; +import android.net.NattKeepalivePacketData; +import android.net.ProxyInfo; +import android.net.TcpKeepalivePacketData; +import android.net.shared.ProvisioningConfiguration; +import android.os.Binder; +import android.os.RemoteException; +import android.util.Log; + +/** + * A convenience wrapper for IpClient. + * + * Wraps IIpClient calls, making them a bit more friendly to use. Currently handles: + * - Clearing calling identity + * - Ignoring RemoteExceptions + * - Converting to stable parcelables + * + * By design, all methods on IIpClient are asynchronous oneway IPCs and are thus void. All the + * wrapper methods in this class return a boolean that callers can use to determine whether + * RemoteException was thrown. + */ +public class IpClientManager { + @NonNull private final IIpClient mIpClient; + @NonNull private final String mTag; + + public IpClientManager(@NonNull IIpClient ipClient, @NonNull String tag) { + mIpClient = ipClient; + mTag = tag; + } + + public IpClientManager(@NonNull IIpClient ipClient) { + this(ipClient, IpClientManager.class.getSimpleName()); + } + + private void log(String s, Throwable e) { + Log.e(mTag, s, e); + } + + /** + * For clients using {@link ProvisioningConfiguration.Builder#withPreDhcpAction()}, must be + * called after {@link IIpClientCallbacks#onPreDhcpAction} to indicate that DHCP is clear to + * proceed. + */ + public boolean completedPreDhcpAction() { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.completedPreDhcpAction(); + return true; + } catch (RemoteException e) { + log("Error completing PreDhcpAction", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Confirm the provisioning configuration. + */ + public boolean confirmConfiguration() { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.confirmConfiguration(); + return true; + } catch (RemoteException e) { + log("Error confirming IpClient configuration", e); + return false; + } + } + + /** + * Indicate that packet filter read is complete. + */ + public boolean readPacketFilterComplete(byte[] data) { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.readPacketFilterComplete(data); + return true; + } catch (RemoteException e) { + log("Error notifying IpClient of packet filter read", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Shut down this IpClient instance altogether. + */ + public boolean shutdown() { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.shutdown(); + return true; + } catch (RemoteException e) { + log("Error shutting down IpClient", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Start provisioning with the provided parameters. + */ + public boolean startProvisioning(ProvisioningConfiguration prov) { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.startProvisioning(prov.toStableParcelable()); + return true; + } catch (RemoteException e) { + log("Error starting IpClient provisioning", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Stop this IpClient. + * + * <p>This does not shut down the StateMachine itself, which is handled by {@link #shutdown()}. + */ + public boolean stop() { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.stop(); + return true; + } catch (RemoteException e) { + log("Error stopping IpClient", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Set the TCP buffer sizes to use. + * + * This may be called, repeatedly, at any time before or after a call to + * #startProvisioning(). The setting is cleared upon calling #stop(). + */ + public boolean setTcpBufferSizes(String tcpBufferSizes) { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.setTcpBufferSizes(tcpBufferSizes); + return true; + } catch (RemoteException e) { + log("Error setting IpClient TCP buffer sizes", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Set the HTTP Proxy configuration to use. + * + * This may be called, repeatedly, at any time before or after a call to + * #startProvisioning(). The setting is cleared upon calling #stop(). + */ + public boolean setHttpProxy(ProxyInfo proxyInfo) { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.setHttpProxy(proxyInfo); + return true; + } catch (RemoteException e) { + log("Error setting IpClient proxy", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Enable or disable the multicast filter. Attempts to use APF to accomplish the filtering, + * if not, Callback.setFallbackMulticastFilter() is called. + */ + public boolean setMulticastFilter(boolean enabled) { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.setMulticastFilter(enabled); + return true; + } catch (RemoteException e) { + log("Error setting multicast filter", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Add a TCP keepalive packet filter before setting up keepalive offload. + */ + public boolean addKeepalivePacketFilter(int slot, TcpKeepalivePacketData pkt) { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.addKeepalivePacketFilter(slot, pkt.toStableParcelable()); + return true; + } catch (RemoteException e) { + log("Error adding Keepalive Packet Filter ", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Add a NAT-T keepalive packet filter before setting up keepalive offload. + */ + public boolean addKeepalivePacketFilter(int slot, NattKeepalivePacketData pkt) { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.addNattKeepalivePacketFilter(slot, pkt.toStableParcelable()); + return true; + } catch (RemoteException e) { + log("Error adding Keepalive Packet Filter ", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Remove a keepalive packet filter after stopping keepalive offload. + */ + public boolean removeKeepalivePacketFilter(int slot) { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.removeKeepalivePacketFilter(slot); + return true; + } catch (RemoteException e) { + log("Error removing Keepalive Packet Filter ", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } + + /** + * Set the L2 key and group hint for storing info into the memory store. + */ + public boolean setL2KeyAndGroupHint(String l2Key, String groupHint) { + final long token = Binder.clearCallingIdentity(); + try { + mIpClient.setL2KeyAndGroupHint(l2Key, groupHint); + return true; + } catch (RemoteException e) { + log("Failed setL2KeyAndGroupHint", e); + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + } +} 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 e60e54c9e44f..04f897ed1bdb 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -380,6 +380,14 @@ public class DisplayContentTests extends WindowTestsBase { assertTrue(window1.isFocused()); assertEquals(perDisplayFocusEnabled && targetSdk >= Q, window2.isFocused()); assertEquals(window1, mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus); + + // Make sure top focused display not changed if there is a focused app. + window1.mAppToken.hiddenRequested = true; + window1.getDisplayContent().setFocusedApp(window1.mAppToken); + updateFocusedWindow(); + assertTrue(!window1.isFocused()); + assertEquals(window1.getDisplayId(), + mWm.mRoot.getTopFocusedDisplayContent().getDisplayId()); } /** diff --git a/telephony/java/android/telephony/PhoneStateListener.java b/telephony/java/android/telephony/PhoneStateListener.java index 3ce28a46a029..271195b78c3e 100644 --- a/telephony/java/android/telephony/PhoneStateListener.java +++ b/telephony/java/android/telephony/PhoneStateListener.java @@ -62,6 +62,8 @@ public class PhoneStateListener { /** * Stop listening for updates. + * + * The PhoneStateListener is not tied to any subscription and unregistered for any update. */ public static final int LISTEN_NONE = 0; @@ -444,7 +446,13 @@ public class PhoneStateListener { } /** - * Callback invoked when device service state changes. + * Callback invoked when device service state changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. * * @see ServiceState#STATE_EMERGENCY_ONLY * @see ServiceState#STATE_IN_SERVICE @@ -456,7 +464,13 @@ public class PhoneStateListener { } /** - * Callback invoked when network signal strength changes. + * Callback invoked when network signal strength changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. * * @see ServiceState#STATE_EMERGENCY_ONLY * @see ServiceState#STATE_IN_SERVICE @@ -470,21 +484,39 @@ public class PhoneStateListener { } /** - * Callback invoked when the message-waiting indicator changes. + * Callback invoked when the message-waiting indicator changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. */ public void onMessageWaitingIndicatorChanged(boolean mwi) { // default implementation empty } /** - * Callback invoked when the call-forwarding indicator changes. + * Callback invoked when the call-forwarding indicator changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. */ public void onCallForwardingIndicatorChanged(boolean cfi) { // default implementation empty } /** - * Callback invoked when device cell location changes. + * Callback invoked when device cell location changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. */ public void onCellLocationChanged(CellLocation location) { // default implementation empty @@ -493,7 +525,14 @@ public class PhoneStateListener { /** * Callback invoked when device call state changes. * <p> - * Reports the state of Telephony (mobile) calls on the device. + * Reports the state of Telephony (mobile) calls on the device for the registered subscription. + * <p> + * Note: the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. * <p> * Note: The state returned here may differ from that returned by * {@link TelephonyManager#getCallState()}. Receivers of this callback should be aware that @@ -511,7 +550,13 @@ public class PhoneStateListener { } /** - * Callback invoked when connection state changes. + * Callback invoked when connection state changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. * * @see TelephonyManager#DATA_DISCONNECTED * @see TelephonyManager#DATA_CONNECTING @@ -529,7 +574,13 @@ public class PhoneStateListener { } /** - * Callback invoked when data activity state changes. + * Callback invoked when data activity state changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. * * @see TelephonyManager#DATA_ACTIVITY_NONE * @see TelephonyManager#DATA_ACTIVITY_IN @@ -542,12 +593,13 @@ public class PhoneStateListener { } /** - * Callback invoked when network signal strengths changes. - * - * @see ServiceState#STATE_EMERGENCY_ONLY - * @see ServiceState#STATE_IN_SERVICE - * @see ServiceState#STATE_OUT_OF_SERVICE - * @see ServiceState#STATE_POWER_OFF + * Callback invoked when network signal strengths changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. */ public void onSignalStrengthsChanged(SignalStrength signalStrength) { // default implementation empty @@ -555,8 +607,15 @@ public class PhoneStateListener { /** - * The Over The Air Service Provisioning (OTASP) has changed. Requires - * the READ_PHONE_STATE permission. + * The Over The Air Service Provisioning (OTASP) has changed on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * + * Requires the READ_PHONE_STATE permission. * @param otaspMode is integer <code>OTASP_UNKNOWN=1<code> * means the value is currently unknown and the system should wait until * <code>OTASP_NEEDED=2<code> or <code>OTASP_NOT_NEEDED=3<code> is received before @@ -570,15 +629,28 @@ public class PhoneStateListener { } /** - * Callback invoked when a observed cell info has changed, - * or new cells have been added or removed. + * Callback invoked when a observed cell info has changed or new cells have been added + * or removed on the registered subscription. + * Note, the registration subId s from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * * @param cellInfo is the list of currently visible cells. */ public void onCellInfoChanged(List<CellInfo> cellInfo) { } /** - * Callback invoked when precise device call state changes. + * Callback invoked when precise device call state changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. * @param callState {@link PreciseCallState} * @hide */ @@ -589,7 +661,14 @@ public class PhoneStateListener { } /** - * Callback invoked when call disconnect cause changes. + * Callback invoked when call disconnect cause changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * * @param disconnectCause {@link DisconnectCause}. * @param preciseDisconnectCause {@link PreciseDisconnectCause}. * @@ -602,7 +681,14 @@ public class PhoneStateListener { } /** - * Callback invoked when Ims call disconnect cause changes. + * Callback invoked when Ims call disconnect cause changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * * @param imsReasonInfo {@link ImsReasonInfo} contains details on why IMS call failed. * * @hide @@ -614,7 +700,15 @@ public class PhoneStateListener { } /** - * Callback invoked when data connection state changes with precise information. + * Callback invoked when data connection state changes with precise information + * on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * * @param dataConnectionState {@link PreciseDataConnectionState} * * @hide @@ -627,7 +721,13 @@ public class PhoneStateListener { } /** - * Callback invoked when data connection state changes with precise information. + * Callback invoked when data connection real time info changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. * * @hide */ @@ -639,7 +739,15 @@ public class PhoneStateListener { /** * Callback invoked when there has been a change in the Single Radio Voice Call Continuity - * (SRVCC) state for the currently active call. + * (SRVCC) state for the currently active call on the registered subscription. + * + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * * @hide */ @SystemApi @@ -648,7 +756,15 @@ public class PhoneStateListener { } /** - * Callback invoked when the SIM voice activation state has changed + * Callback invoked when the SIM voice activation state has changed on the registered + * subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * * @param state is the current SIM voice activation state * @hide */ @@ -657,7 +773,15 @@ public class PhoneStateListener { } /** - * Callback invoked when the SIM data activation state has changed + * Callback invoked when the SIM data activation state has changed on the registered + * subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * * @param state is the current SIM data activation state * @hide */ @@ -665,7 +789,14 @@ public class PhoneStateListener { } /** - * Callback invoked when the user mobile data state has changed + * Callback invoked when the user mobile data state has changed on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * * @param enabled indicates whether the current user mobile data state is enabled or disabled. */ public void onUserMobileDataStateChanged(boolean enabled) { @@ -673,7 +804,14 @@ public class PhoneStateListener { } /** - * Callback invoked when the current physical channel configuration has changed + * Callback invoked when the current physical channel configuration has changed on the + * registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. * * @param configs List of the current {@link PhysicalChannelConfig}s * @hide @@ -684,7 +822,14 @@ public class PhoneStateListener { } /** - * Callback invoked when the current emergency number list has changed + * Callback invoked when the current emergency number list has changed on the registered + * subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. * * @param emergencyNumberList Map including the key as the active subscription ID * (Note: if there is no active subscription, the key is @@ -699,8 +844,15 @@ public class PhoneStateListener { } /** - * Callback invoked when OEM hook raw event is received. Requires - * the READ_PRIVILEGED_PHONE_STATE permission. + * Callback invoked when OEM hook raw event is received on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * + * Requires the READ_PRIVILEGED_PHONE_STATE permission. * @param rawData is the byte array of the OEM hook raw data. * @hide */ @@ -710,8 +862,10 @@ public class PhoneStateListener { } /** - * Callback invoked when phone capability changes. Requires - * the READ_PRIVILEGED_PHONE_STATE permission. + * Callback invoked when phone capability changes. + * Note, this callback triggers regardless of registered subscription. + * + * Requires the READ_PRIVILEGED_PHONE_STATE permission. * @param capability the new phone capability * @hide */ @@ -720,8 +874,10 @@ public class PhoneStateListener { } /** - * Callback invoked when active data subId changes. Requires - * the READ_PHONE_STATE permission. + * Callback invoked when active data subId changes. + * Note, this callback triggers regardless of registered subscription. + * + * Requires the READ_PHONE_STATE permission. * @param subId current subscription used to setup Cellular Internet data. * For example, it could be the current active opportunistic subscription in use, * or the subscription user selected as default data subscription in DSDS mode. @@ -731,8 +887,15 @@ public class PhoneStateListener { } /** - * Callback invoked when the call attributes changes. Requires - * the READ_PRIVILEGED_PHONE_STATE permission. + * Callback invoked when the call attributes changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * + * Requires the READ_PRIVILEGED_PHONE_STATE permission. * @param callAttributes the call attributes * @hide */ @@ -742,7 +905,15 @@ public class PhoneStateListener { } /** - * Callback invoked when modem radio power state changes. Requires + * Callback invoked when modem radio power state changes on the registered subscription. + * Note, the registration subId comes from {@link TelephonyManager} object which registers + * PhoneStateListener by {@link TelephonyManager#listen(PhoneStateListener, int)}. + * If this TelephonyManager object was created with + * {@link TelephonyManager#createForSubscriptionId(int)}, then the callback applies to the + * subId. Otherwise, this callback applies to + * {@link SubscriptionManager#getDefaultSubscriptionId()}. + * + * Requires * the READ_PRIVILEGED_PHONE_STATE permission. * @param state the modem radio power state * @hide @@ -758,6 +929,10 @@ public class PhoneStateListener { * has been requested by an app using * {@link android.telephony.TelephonyManager#notifyCarrierNetworkChange(boolean)} * + * Note, this callback is pinned to the registered subscription and will be invoked when + * the notifying carrier app has carrier privilege rule on the registered + * subscription. {@link android.telephony.TelephonyManager#hasCarrierPrivileges} + * * @param active Whether the carrier network change is or shortly * will be active. This value is true to indicate * showing alternative UI and false to stop. diff --git a/telephony/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/telephony/java/com/android/internal/telephony/ITelephonyRegistry.aidl index 0610c5d106c3..f2f3c2d85fd4 100644 --- a/telephony/java/com/android/internal/telephony/ITelephonyRegistry.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephonyRegistry.aidl @@ -56,38 +56,41 @@ interface ITelephonyRegistry { void notifyDataConnection(int state, boolean isDataConnectivityPossible, String apn, String apnType, in LinkProperties linkProperties, in NetworkCapabilities networkCapabilities, int networkType, boolean roaming); - void notifyDataConnectionForSubscriber(int subId, int state, boolean isDataConnectivityPossible, + void notifyDataConnectionForSubscriber(int phoneId, int subId, int state, + boolean isDataConnectivityPossible, String apn, String apnType, in LinkProperties linkProperties, in NetworkCapabilities networkCapabilities, int networkType, boolean roaming); @UnsupportedAppUsage void notifyDataConnectionFailed(String apnType); - void notifyDataConnectionFailedForSubscriber(int subId, String apnType); + void notifyDataConnectionFailedForSubscriber(int phoneId, int subId, String apnType); void notifyCellLocation(in Bundle cellLocation); void notifyCellLocationForSubscriber(in int subId, in Bundle cellLocation); - void notifyOtaspChanged(in int otaspMode); + void notifyOtaspChanged(in int subId, in int otaspMode); @UnsupportedAppUsage void notifyCellInfo(in List<CellInfo> cellInfo); void notifyPhysicalChannelConfiguration(in List<PhysicalChannelConfig> configs); void notifyPhysicalChannelConfigurationForSubscriber(in int subId, in List<PhysicalChannelConfig> configs); - void notifyPreciseCallState(int ringingCallState, int foregroundCallState, - int backgroundCallState, int phoneId); - void notifyDisconnectCause(int disconnectCause, int preciseDisconnectCause); - void notifyPreciseDataConnectionFailed(String apnType, String apn, + void notifyPreciseCallState(int phoneId, int subId, int ringingCallState, + int foregroundCallState, int backgroundCallState); + void notifyDisconnectCause(int phoneId, int subId, int disconnectCause, + int preciseDisconnectCause); + void notifyPreciseDataConnectionFailed(int phoneId, int subId, String apnType, String apn, int failCause); void notifyCellInfoForSubscriber(in int subId, in List<CellInfo> cellInfo); void notifySrvccStateChanged(in int subId, in int lteState); void notifySimActivationStateChangedForPhoneId(in int phoneId, in int subId, int activationState, int activationType); - void notifyOemHookRawEventForSubscriber(in int subId, in byte[] rawData); + void notifyOemHookRawEventForSubscriber(in int phoneId, in int subId, in byte[] rawData); void notifySubscriptionInfoChanged(); void notifyOpportunisticSubscriptionInfoChanged(); void notifyCarrierNetworkChange(in boolean active); void notifyUserMobileDataStateChangedForPhoneId(in int phoneId, in int subId, in boolean state); void notifyPhoneCapabilityChanged(in PhoneCapability capability); void notifyActiveDataSubIdChanged(int activeDataSubId); - void notifyRadioPowerStateChanged(in int state); - void notifyEmergencyNumberList(); - void notifyCallQualityChanged(in CallQuality callQuality, int phoneId, int callNetworkType); + void notifyRadioPowerStateChanged(in int phoneId, in int subId, in int state); + void notifyEmergencyNumberList(in int phoneId, in int subId); + void notifyCallQualityChanged(in CallQuality callQuality, int phoneId, int subId, + int callNetworkType); void notifyImsDisconnectCause(int subId, in ImsReasonInfo imsReasonInfo); } diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java index 363ac9ce1d25..0ef56e553aa9 100644 --- a/tests/net/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java @@ -3854,6 +3854,9 @@ public class ConnectivityServiceTest { networkCallback.expectCallback(CallbackState.UNAVAILABLE, null); testFactory.waitForRequests(); + // unregister network callback - a no-op, but should not fail + mCm.unregisterNetworkCallback(networkCallback); + testFactory.unregister(); handlerThread.quit(); } diff --git a/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java index 7c40adfac002..71b72b84de81 100644 --- a/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java +++ b/tests/net/java/com/android/server/IpSecServiceParameterizedTest.java @@ -32,6 +32,7 @@ import static org.mockito.Mockito.when; import android.app.AppOpsManager; import android.content.Context; +import android.content.pm.PackageManager; import android.net.INetd; import android.net.IpSecAlgorithm; import android.net.IpSecConfig; @@ -57,6 +58,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import java.net.Inet4Address; import java.net.Socket; import java.util.Arrays; import java.util.Collection; @@ -119,6 +121,11 @@ public class IpSecServiceParameterizedTest { } @Override + public PackageManager getPackageManager() { + return mMockPkgMgr; + } + + @Override public void enforceCallingOrSelfPermission(String permission, String message) { if (permission == android.Manifest.permission.MANAGE_IPSEC_TUNNELS) { return; @@ -128,6 +135,7 @@ public class IpSecServiceParameterizedTest { }; INetd mMockNetd; + PackageManager mMockPkgMgr; IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig; IpSecService mIpSecService; Network fakeNetwork = new Network(0xAB); @@ -152,11 +160,16 @@ public class IpSecServiceParameterizedTest { @Before public void setUp() throws Exception { mMockNetd = mock(INetd.class); + mMockPkgMgr = mock(PackageManager.class); mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class); mIpSecService = new IpSecService(mMockContext, mMockIpSecSrvConfig); // Injecting mock netd when(mMockIpSecSrvConfig.getNetdInstance()).thenReturn(mMockNetd); + + // PackageManager should always return true (feature flag tests in IpSecServiceTest) + when(mMockPkgMgr.hasSystemFeature(anyString())).thenReturn(true); + // A package granted the AppOp for MANAGE_IPSEC_TUNNELS will be MODE_ALLOWED. when(mMockAppOps.noteOp(anyInt(), anyInt(), eq("blessedPackage"))) .thenReturn(AppOpsManager.MODE_ALLOWED); @@ -709,4 +722,18 @@ public class IpSecServiceParameterizedTest { } catch (SecurityException expected) { } } + + @Test + public void testFeatureFlagVerification() throws Exception { + when(mMockPkgMgr.hasSystemFeature(eq(PackageManager.FEATURE_IPSEC_TUNNELS))) + .thenReturn(false); + + try { + String addr = Inet4Address.getLoopbackAddress().getHostAddress(); + mIpSecService.createTunnelInterface( + addr, addr, new Network(0), new Binder(), "blessedPackage"); + fail("Expected UnsupportedOperationException for disabled feature"); + } catch (UnsupportedOperationException expected) { + } + } } |