diff options
65 files changed, 1904 insertions, 1685 deletions
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/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 7d34381f73fa..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 { 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/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/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/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/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/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/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/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..2d0944ad246f 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; @@ -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/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 123d73dc6432..35dc1775cb7f 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -549,6 +549,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 +571,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 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/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 6bb1cfaf11c2..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; diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index e61fa326215f..66b5437f0a7d 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -2407,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 @@ -2538,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) { @@ -2693,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); + } } } @@ -3270,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 225c08092b49..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; @@ -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,24 +1982,21 @@ 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); 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/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/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index ed83cbced49f..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) { @@ -21531,6 +21530,7 @@ public class PackageManagerService extends IPackageManager.Stub storage.registerListener(mStorageListener); mInstallerService.systemReady(); + mApexManager.systemReady(); mPackageDexOptimizer.systemReady(); getStorageManagerInternal().addExternalStoragePolicy( 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/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) { + } + } } |