diff options
171 files changed, 6189 insertions, 1595 deletions
diff --git a/api/current.txt b/api/current.txt index 8a20da21e903..689f4d4ce18b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5200,6 +5200,7 @@ package android.app { field public static final java.lang.String EXTRA_CONVERSATION_TITLE = "android.conversationTitle"; field public static final java.lang.String EXTRA_HISTORIC_MESSAGES = "android.messages.historic"; field public static final java.lang.String EXTRA_INFO_TEXT = "android.infoText"; + field public static final java.lang.String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation"; field public static final deprecated java.lang.String EXTRA_LARGE_ICON = "android.largeIcon"; field public static final java.lang.String EXTRA_LARGE_ICON_BIG = "android.largeIcon.big"; field public static final java.lang.String EXTRA_MEDIA_SESSION = "android.mediaSession"; @@ -5483,7 +5484,9 @@ package android.app { method public java.util.List<android.app.Notification.MessagingStyle.Message> getHistoricMessages(); method public java.util.List<android.app.Notification.MessagingStyle.Message> getMessages(); method public java.lang.CharSequence getUserDisplayName(); + method public boolean isGroupConversation(); method public android.app.Notification.MessagingStyle setConversationTitle(java.lang.CharSequence); + method public android.app.Notification.MessagingStyle setGroupConversation(boolean); field public static final int MAXIMUM_RETAINED_MESSAGES = 25; // 0x19 } @@ -6451,6 +6454,7 @@ package android.app.admin { method public void setDeviceOwnerLockScreenInfo(android.content.ComponentName, java.lang.CharSequence); method public void setGlobalSetting(android.content.ComponentName, java.lang.String, java.lang.String); method public void setKeepUninstalledPackages(android.content.ComponentName, java.util.List<java.lang.String>); + method public boolean setKeyPairCertificate(android.content.ComponentName, java.lang.String, java.util.List<java.security.cert.Certificate>, boolean); method public boolean setKeyguardDisabled(android.content.ComponentName, boolean); method public void setKeyguardDisabledFeatures(android.content.ComponentName, int); method public void setLockTaskFeatures(android.content.ComponentName, int); @@ -41065,6 +41069,7 @@ package android.telephony { method public java.lang.String getMeid(int); method public java.lang.String getMmsUAProfUrl(); method public java.lang.String getMmsUserAgent(); + method public java.lang.String getNai(); method public deprecated java.util.List<android.telephony.NeighboringCellInfo> getNeighboringCellInfo(); method public java.lang.String getNetworkCountryIso(); method public java.lang.String getNetworkOperator(); @@ -41908,9 +41913,9 @@ package android.text { } public class DynamicLayout extends android.text.Layout { - ctor public DynamicLayout(java.lang.CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean); - ctor public DynamicLayout(java.lang.CharSequence, java.lang.CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean); - ctor public DynamicLayout(java.lang.CharSequence, java.lang.CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean, android.text.TextUtils.TruncateAt, int); + ctor public deprecated DynamicLayout(java.lang.CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean); + ctor public deprecated DynamicLayout(java.lang.CharSequence, java.lang.CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean); + ctor public deprecated DynamicLayout(java.lang.CharSequence, java.lang.CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean, android.text.TextUtils.TruncateAt, int); method public int getBottomPadding(); method public int getEllipsisCount(int); method public int getEllipsisStart(int); @@ -42297,9 +42302,9 @@ package android.text { } public class StaticLayout extends android.text.Layout { - ctor public StaticLayout(java.lang.CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean); - ctor public StaticLayout(java.lang.CharSequence, int, int, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean); - ctor public StaticLayout(java.lang.CharSequence, int, int, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean, android.text.TextUtils.TruncateAt, int); + ctor public deprecated StaticLayout(java.lang.CharSequence, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean); + ctor public deprecated StaticLayout(java.lang.CharSequence, int, int, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean); + ctor public deprecated StaticLayout(java.lang.CharSequence, int, int, android.text.TextPaint, int, android.text.Layout.Alignment, float, float, boolean, android.text.TextUtils.TruncateAt, int); method public int getBottomPadding(); method public int getEllipsisCount(int); method public int getEllipsisStart(int); @@ -44716,6 +44721,14 @@ package android.view { field public static final android.os.Parcelable.Creator<android.view.Display.Mode> CREATOR; } + public final class DisplayCutout { + method public android.graphics.Region getBounds(); + method public int getSafeInsetBottom(); + method public int getSafeInsetLeft(); + method public int getSafeInsetRight(); + method public int getSafeInsetTop(); + } + public final class DragAndDropPermissions implements android.os.Parcelable { method public int describeContents(); method public void release(); @@ -47669,8 +47682,10 @@ package android.view { public final class WindowInsets { ctor public WindowInsets(android.view.WindowInsets); + method public android.view.WindowInsets consumeDisplayCutout(); method public android.view.WindowInsets consumeStableInsets(); method public android.view.WindowInsets consumeSystemWindowInsets(); + method public android.view.DisplayCutout getDisplayCutout(); method public int getStableInsetBottom(); method public int getStableInsetLeft(); method public int getStableInsetRight(); @@ -47730,6 +47745,7 @@ package android.view { field public static final int FIRST_APPLICATION_WINDOW = 1; // 0x1 field public static final int FIRST_SUB_WINDOW = 1000; // 0x3e8 field public static final int FIRST_SYSTEM_WINDOW = 2000; // 0x7d0 + field public static final long FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA = 1L; // 0x1L field public static final int FLAGS_CHANGED = 4; // 0x4 field public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON = 1; // 0x1 field public static final int FLAG_ALT_FOCUSABLE_IM = 131072; // 0x20000 @@ -47824,6 +47840,7 @@ package android.view { field public float buttonBrightness; field public float dimAmount; field public int flags; + field public long flags2; field public int format; field public int gravity; field public float horizontalMargin; diff --git a/cmds/statsd/src/packages/UidMap.cpp b/cmds/statsd/src/packages/UidMap.cpp index 6e7a613b05a3..3018be145ad8 100644 --- a/cmds/statsd/src/packages/UidMap.cpp +++ b/cmds/statsd/src/packages/UidMap.cpp @@ -31,10 +31,9 @@ namespace android { namespace os { namespace statsd { -UidMap::UidMap() : mBytesUsed(0) { -} -UidMap::~UidMap() { -} +UidMap::UidMap() : mBytesUsed(0) {} + +UidMap::~UidMap() {} bool UidMap::hasApp(int uid, const string& packageName) const { lock_guard<mutex> lock(mMutex); @@ -48,6 +47,27 @@ bool UidMap::hasApp(int uid, const string& packageName) const { return false; } +string UidMap::normalizeAppName(const string& appName) const { + string normalizedName = appName; + std::transform(normalizedName.begin(), normalizedName.end(), normalizedName.begin(), ::tolower); + return normalizedName; +} + +std::set<string> UidMap::getAppNamesFromUid(const int32_t& uid, bool returnNormalized) const { + lock_guard<mutex> lock(mMutex); + return getAppNamesFromUidLocked(uid,returnNormalized); +} + +std::set<string> UidMap::getAppNamesFromUidLocked(const int32_t& uid, bool returnNormalized) const { + std::set<string> names; + auto range = mMap.equal_range(uid); + for (auto it = range.first; it != range.second; ++it) { + names.insert(returnNormalized ? + normalizeAppName(it->second.packageName) : it->second.packageName); + } + return names; +} + int64_t UidMap::getAppVersion(int uid, const string& packageName) const { lock_guard<mutex> lock(mMutex); @@ -97,17 +117,17 @@ void UidMap::updateApp(const int64_t& timestamp, const String16& app_16, const i const int64_t& versionCode) { lock_guard<mutex> lock(mMutex); - string app = string(String8(app_16).string()); + string appName = string(String8(app_16).string()); // Notify any interested producers that this app has updated for (auto it : mSubscribers) { - it->notifyAppUpgrade(app, uid, versionCode); + it->notifyAppUpgrade(appName, uid, versionCode); } auto log = mOutput.add_changes(); log->set_deletion(false); log->set_timestamp_nanos(timestamp); - log->set_app(app); + log->set_app(appName); log->set_uid(uid); log->set_version(versionCode); mBytesUsed += log->ByteSize(); @@ -117,16 +137,15 @@ void UidMap::updateApp(const int64_t& timestamp, const String16& app_16, const i auto range = mMap.equal_range(int(uid)); for (auto it = range.first; it != range.second; ++it) { - if (it->second.packageName == app) { + // If we find the exact same app name and uid, update the app version directly. + if (it->second.packageName == appName) { it->second.versionCode = versionCode; return; } - VLOG("updateApp failed to find the app %s with uid %i to update", app.c_str(), uid); - return; } // Otherwise, we need to add an app at this uid. - mMap.insert(make_pair(uid, AppData(app, versionCode))); + mMap.insert(make_pair(uid, AppData(appName, versionCode))); } void UidMap::ensureBytesUsedBelowLimit() { @@ -154,6 +173,7 @@ void UidMap::ensureBytesUsedBelowLimit() { void UidMap::removeApp(const String16& app_16, const int32_t& uid) { removeApp(time(nullptr) * NS_PER_SEC, app_16, uid); } + void UidMap::removeApp(const int64_t& timestamp, const String16& app_16, const int32_t& uid) { lock_guard<mutex> lock(mMutex); diff --git a/cmds/statsd/src/packages/UidMap.h b/cmds/statsd/src/packages/UidMap.h index 9e1ad6946af2..487fdf945f20 100644 --- a/cmds/statsd/src/packages/UidMap.h +++ b/cmds/statsd/src/packages/UidMap.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef STATSD_UIDMAP_H -#define STATSD_UIDMAP_H +#pragma once #include "config/ConfigKey.h" #include "config/ConfigListener.h" @@ -66,6 +65,9 @@ public: // Returns true if the given uid contains the specified app (eg. com.google.android.gms). bool hasApp(int uid, const string& packageName) const; + // Returns the app names from uid. + std::set<string> getAppNamesFromUid(const int32_t& uid, bool returnNormalized) const; + int64_t getAppVersion(int uid, const string& packageName) const; // Helper for debugging contents of this uid map. Can be triggered with: @@ -103,6 +105,9 @@ public: size_t getBytesUsed(); private: + std::set<string> getAppNamesFromUidLocked(const int32_t& uid, bool returnNormalized) const; + string normalizeAppName(const string& appName) const; + void updateMap(const int64_t& timestamp, const vector<int32_t>& uid, const vector<int64_t>& versionCode, const vector<String16>& packageName); @@ -160,4 +165,3 @@ private: } // namespace os } // namespace android -#endif // STATSD_UIDMAP_H diff --git a/cmds/statsd/tests/UidMap_test.cpp b/cmds/statsd/tests/UidMap_test.cpp index 5b2ceddc6858..3fa96d392927 100644 --- a/cmds/statsd/tests/UidMap_test.cpp +++ b/cmds/statsd/tests/UidMap_test.cpp @@ -74,6 +74,14 @@ TEST(UidMapTest, TestMatching) { EXPECT_TRUE(m.hasApp(1000, kApp1)); EXPECT_TRUE(m.hasApp(1000, kApp2)); EXPECT_FALSE(m.hasApp(1000, "not.app")); + + std::set<string> name_set = m.getAppNamesFromUid(1000u, true /* returnNormalized */); + EXPECT_EQ(name_set.size(), 2u); + EXPECT_TRUE(name_set.find(kApp1) != name_set.end()); + EXPECT_TRUE(name_set.find(kApp2) != name_set.end()); + + name_set = m.getAppNamesFromUid(12345, true /* returnNormalized */); + EXPECT_TRUE(name_set.empty()); } TEST(UidMapTest, TestAddAndRemove) { @@ -90,12 +98,59 @@ TEST(UidMapTest, TestAddAndRemove) { versions.push_back(5); m.updateMap(uids, versions, apps); + std::set<string> name_set = m.getAppNamesFromUid(1000, true /* returnNormalized */); + EXPECT_EQ(name_set.size(), 2u); + EXPECT_TRUE(name_set.find(kApp1) != name_set.end()); + EXPECT_TRUE(name_set.find(kApp2) != name_set.end()); + + // Update the app1 version. m.updateApp(String16(kApp1.c_str()), 1000, 40); EXPECT_EQ(40, m.getAppVersion(1000, kApp1)); + name_set = m.getAppNamesFromUid(1000, true /* returnNormalized */); + EXPECT_EQ(name_set.size(), 2u); + EXPECT_TRUE(name_set.find(kApp1) != name_set.end()); + EXPECT_TRUE(name_set.find(kApp2) != name_set.end()); + m.removeApp(String16(kApp1.c_str()), 1000); EXPECT_FALSE(m.hasApp(1000, kApp1)); EXPECT_TRUE(m.hasApp(1000, kApp2)); + name_set = m.getAppNamesFromUid(1000, true /* returnNormalized */); + EXPECT_EQ(name_set.size(), 1u); + EXPECT_TRUE(name_set.find(kApp1) == name_set.end()); + EXPECT_TRUE(name_set.find(kApp2) != name_set.end()); + + // Remove app2. + m.removeApp(String16(kApp2.c_str()), 1000); + EXPECT_FALSE(m.hasApp(1000, kApp1)); + EXPECT_FALSE(m.hasApp(1000, kApp2)); + name_set = m.getAppNamesFromUid(1000, true /* returnNormalized */); + EXPECT_TRUE(name_set.empty()); +} + +TEST(UidMapTest, TestUpdateApp) { + UidMap m; + m.updateMap({1000, 1000}, {4, 5}, {String16(kApp1.c_str()), String16(kApp2.c_str())}); + std::set<string> name_set = m.getAppNamesFromUid(1000, true /* returnNormalized */); + EXPECT_EQ(name_set.size(), 2u); + EXPECT_TRUE(name_set.find(kApp1) != name_set.end()); + EXPECT_TRUE(name_set.find(kApp2) != name_set.end()); + + // Adds a new name for uid 1000. + m.updateApp(String16("NeW_aPP1_NAmE"), 1000, 40); + name_set = m.getAppNamesFromUid(1000, true /* returnNormalized */); + EXPECT_EQ(name_set.size(), 3u); + EXPECT_TRUE(name_set.find(kApp1) != name_set.end()); + EXPECT_TRUE(name_set.find(kApp2) != name_set.end()); + EXPECT_TRUE(name_set.find("NeW_aPP1_NAmE") == name_set.end()); + EXPECT_TRUE(name_set.find("new_app1_name") != name_set.end()); + + // This name is also reused by another uid 2000. + m.updateApp(String16("NeW_aPP1_NAmE"), 2000, 1); + name_set = m.getAppNamesFromUid(2000, true /* returnNormalized */); + EXPECT_EQ(name_set.size(), 1u); + EXPECT_TRUE(name_set.find("NeW_aPP1_NAmE") == name_set.end()); + EXPECT_TRUE(name_set.find("new_app1_name") != name_set.end()); } TEST(UidMapTest, TestClearingOutput) { diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 705f9a0513f6..85c3be826c0d 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1090,6 +1090,12 @@ public class Notification implements Parcelable public static final String EXTRA_HISTORIC_MESSAGES = "android.messages.historic"; /** + * {@link #extras} key: whether the {@link android.app.Notification.MessagingStyle} notification + * represents a group conversation. + */ + public static final String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation"; + + /** * {@link #extras} key: whether the notification should be colorized as * supplied to {@link Builder#setColorized(boolean)}}. */ @@ -5960,9 +5966,10 @@ public class Notification implements Parcelable public static final int MAXIMUM_RETAINED_MESSAGES = 25; CharSequence mUserDisplayName; - CharSequence mConversationTitle; + @Nullable CharSequence mConversationTitle; List<Message> mMessages = new ArrayList<>(); List<Message> mHistoricMessages = new ArrayList<>(); + boolean mIsGroupConversation; MessagingStyle() { } @@ -5985,20 +5992,20 @@ public class Notification implements Parcelable } /** - * Sets the title to be displayed on this conversation. This should only be used for - * group messaging and left unset for one-on-one conversations. - * @param conversationTitle + * Sets the title to be displayed on this conversation. May be set to {@code null}. + * + * @param conversationTitle A name for the conversation, or {@code null} * @return this object for method chaining. */ - public MessagingStyle setConversationTitle(CharSequence conversationTitle) { + public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) { mConversationTitle = conversationTitle; return this; } /** - * Return the title to be displayed on this conversation. Can be <code>null</code> and - * should be for one-on-one conversations + * Return the title to be displayed on this conversation. May return {@code null}. */ + @Nullable public CharSequence getConversationTitle() { return mConversationTitle; } @@ -6075,6 +6082,24 @@ public class Notification implements Parcelable } /** + * Sets whether this conversation notification represents a group. + * @param isGroupConversation {@code true} if the conversation represents a group, + * {@code false} otherwise. + * @return this object for method chaining + */ + public MessagingStyle setGroupConversation(boolean isGroupConversation) { + mIsGroupConversation = isGroupConversation; + return this; + } + + /** + * Returns {@code true} if this notification represents a group conversation. + */ + public boolean isGroupConversation() { + return mIsGroupConversation; + } + + /** * @hide */ @Override @@ -6094,6 +6119,7 @@ public class Notification implements Parcelable } fixTitleAndTextExtras(extras); + extras.putBoolean(EXTRA_IS_GROUP_CONVERSATION, mIsGroupConversation); } private void fixTitleAndTextExtras(Bundle extras) { @@ -6136,6 +6162,7 @@ public class Notification implements Parcelable mMessages = Message.getMessagesFromBundleArray(messages); Parcelable[] histMessages = extras.getParcelableArray(EXTRA_HISTORIC_MESSAGES); mHistoricMessages = Message.getMessagesFromBundleArray(histMessages); + mIsGroupConversation = extras.getBoolean(EXTRA_IS_GROUP_CONVERSATION); } /** diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 9ad990af3bb5..98162976173d 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -4174,6 +4174,52 @@ public class DevicePolicyManager { return null; } + + /** + * Called by a device or profile owner, or delegated certificate installer, to associate + * certificates with a key pair that was generated using {@link #generateKeyPair}, and + * set whether the key is available for the user to choose in the certificate selection + * prompt. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with, or + * {@code null} if calling from a delegated certificate installer. + * @param alias The private key alias under which to install the certificate. The {@code alias} + * should denote an existing private key. If a certificate with that alias already + * exists, it will be overwritten. + * @param certs The certificate chain to install. The chain should start with the leaf + * certificate and include the chain of trust in order. This will be returned by + * {@link android.security.KeyChain#getCertificateChain}. + * @param isUserSelectable {@code true} to indicate that a user can select this key via the + * certificate selection prompt, {@code false} to indicate that this key can only be + * granted access by implementing + * {@link android.app.admin.DeviceAdminReceiver#onChoosePrivateKeyAlias}. + * @return {@code true} if the provided {@code alias} exists and the certificates has been + * successfully associated with it, {@code false} otherwise. + * @throws SecurityException if {@code admin} is not {@code null} and not a device or profile + * owner, or {@code admin} is null but the calling application is not a delegated + * certificate installer. + */ + public boolean setKeyPairCertificate(@Nullable ComponentName admin, + @NonNull String alias, @NonNull List<Certificate> certs, boolean isUserSelectable) { + throwIfParentInstance("setKeyPairCertificate"); + try { + final byte[] pemCert = Credentials.convertToPem(certs.get(0)); + byte[] pemChain = null; + if (certs.size() > 1) { + pemChain = Credentials.convertToPem( + certs.subList(1, certs.size()).toArray(new Certificate[0])); + } + return mService.setKeyPairCertificate(admin, mContext.getPackageName(), alias, pemCert, + pemChain, isUserSelectable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } catch (CertificateException | IOException e) { + Log.w(TAG, "Could not pem-encode certificate", e); + } + return false; + } + + /** * @return the alias of a given CA certificate in the certificate store, or {@code null} if it * doesn't exist. diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index f4cd797438ae..5b02c221332a 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -175,6 +175,8 @@ interface IDevicePolicyManager { boolean generateKeyPair(in ComponentName who, in String callerPackage, in String algorithm, in ParcelableKeyGenParameterSpec keySpec, out KeymasterCertificateChain attestationChain); + boolean setKeyPairCertificate(in ComponentName who, in String callerPackage, in String alias, + in byte[] certBuffer, in byte[] certChainBuffer, boolean isUserSelectable); void choosePrivateKeyAlias(int uid, in Uri uri, in String alias, IBinder aliasCallback); void setDelegatedScopes(in ComponentName who, in String delegatePackage, in List<String> scopes); diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java index 7409671f9b9f..a85b5f710696 100644 --- a/core/java/android/hardware/camera2/params/OutputConfiguration.java +++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java @@ -486,6 +486,7 @@ public final class OutputConfiguration implements Parcelable { this.mConfiguredSize = other.mConfiguredSize; this.mConfiguredGenerationId = other.mConfiguredGenerationId; this.mIsDeferredConfig = other.mIsDeferredConfig; + this.mIsShared = other.mIsShared; } /** @@ -498,6 +499,7 @@ public final class OutputConfiguration implements Parcelable { int width = source.readInt(); int height = source.readInt(); boolean isDeferred = source.readInt() == 1; + boolean isShared = source.readInt() == 1; ArrayList<Surface> surfaces = new ArrayList<Surface>(); source.readTypedList(surfaces, Surface.CREATOR); @@ -508,6 +510,7 @@ public final class OutputConfiguration implements Parcelable { mSurfaces = surfaces; mConfiguredSize = new Size(width, height); mIsDeferredConfig = isDeferred; + mIsShared = isShared; mSurfaces = surfaces; if (mSurfaces.size() > 0) { mSurfaceType = SURFACE_TYPE_UNKNOWN; diff --git a/core/java/android/hardware/location/ContextHubInfo.java b/core/java/android/hardware/location/ContextHubInfo.java index c25f151b5c03..c2b280016f68 100644 --- a/core/java/android/hardware/location/ContextHubInfo.java +++ b/core/java/android/hardware/location/ContextHubInfo.java @@ -262,7 +262,7 @@ public class ContextHubInfo implements Parcelable { @Override public String toString() { String retVal = ""; - retVal += "Id : " + mId; + retVal += "ID/handle : " + mId; retVal += ", Name : " + mName; retVal += "\n\tVendor : " + mVendor; retVal += ", Toolchain : " + mToolchain; @@ -275,8 +275,6 @@ public class ContextHubInfo implements Parcelable { retVal += ", StoppedPowerDraw : " + mStoppedPowerDrawMw + " mW"; retVal += ", PeakPowerDraw : " + mPeakPowerDrawMw + " mW"; retVal += ", MaxPacketLength : " + mMaxPacketLengthBytes + " Bytes"; - retVal += "\n\tSupported sensors : " + Arrays.toString(mSupportedSensors); - retVal += "\n\tMemory Regions : " + Arrays.toString(mMemoryRegions); return retVal; } diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java index 5b89f54b5050..6da6fb7bfdb7 100644 --- a/core/java/android/hardware/location/ContextHubManager.java +++ b/core/java/android/hardware/location/ContextHubManager.java @@ -258,9 +258,9 @@ public final class ContextHubManager { } /** - * Returns the list of context hubs in the system. + * Returns the list of ContextHubInfo objects describing the available Context Hubs. * - * @return the list of context hub informations + * @return the list of ContextHubInfo objects * * @see ContextHubInfo * @@ -268,7 +268,11 @@ public final class ContextHubManager { */ @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE) public List<ContextHubInfo> getContextHubs() { - throw new UnsupportedOperationException("TODO: Implement this"); + try { + return mService.getContextHubs(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } /** diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl index db5bd36e1109..233e857d8e67 100644 --- a/core/java/android/hardware/location/IContextHubService.aidl +++ b/core/java/android/hardware/location/IContextHubService.aidl @@ -43,23 +43,26 @@ interface IContextHubService { ContextHubInfo getContextHubInfo(int contextHubHandle); // Loads a nanoapp at the specified hub (old API) - int loadNanoApp(int hubHandle, in NanoApp app); + int loadNanoApp(int contextHubHandle, in NanoApp nanoApp); // Unloads a nanoapp given its instance ID (old API) - int unloadNanoApp(int nanoAppInstanceHandle); + int unloadNanoApp(int nanoAppHandle); // Gets the NanoAppInstanceInfo of a nanoapp give its instance ID - NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppInstanceHandle); + NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle); // Finds all nanoApp instances matching some filter - int[] findNanoAppOnHub(int hubHandle, in NanoAppFilter filter); + int[] findNanoAppOnHub(int contextHubHandle, in NanoAppFilter filter); // Sends a message to a nanoApp - int sendMessage(int hubHandle, int nanoAppHandle, in ContextHubMessage msg); + int sendMessage(int contextHubHandle, int nanoAppHandle, in ContextHubMessage msg); // Creates a client to send and receive messages IContextHubClient createClient(in IContextHubClientCallback client, int contextHubId); + // Returns a list of ContextHub objects of available hubs + List<ContextHubInfo> getContextHubs(); + // Loads a nanoapp at the specified hub (new API) void loadNanoAppOnHub( int contextHubId, in IContextHubTransactionCallback transactionCallback, diff --git a/core/java/android/hardware/location/NanoAppInstanceInfo.java b/core/java/android/hardware/location/NanoAppInstanceInfo.java index b7e6b66fb755..f73fd87b1a19 100644 --- a/core/java/android/hardware/location/NanoAppInstanceInfo.java +++ b/core/java/android/hardware/location/NanoAppInstanceInfo.java @@ -27,15 +27,13 @@ import libcore.util.EmptyArray; * Describes an instance of a nanoapp, used by the internal state manged by ContextHubService. * * TODO(b/69270990) Remove this class once the old API is deprecated. - * TODO(b/70624255) Clean up toString() by removing unnecessary fields * * @hide */ @SystemApi public class NanoAppInstanceInfo { - private static final String PRE_LOADED_GENERIC_UNKNOWN = "Preloaded app, unknown"; - private String mPublisher = PRE_LOADED_GENERIC_UNKNOWN; - private String mName = PRE_LOADED_GENERIC_UNKNOWN; + private String mPublisher = "Unknown"; + private String mName = "Unknown"; private int mHandle; private long mAppId; @@ -227,9 +225,7 @@ public class NanoAppInstanceInfo { public String toString() { String retVal = "handle : " + mHandle; retVal += ", Id : 0x" + Long.toHexString(mAppId); - retVal += ", Version : " + mAppVersion; - retVal += ", Name : " + mName; - retVal += ", Publisher : " + mPublisher; + retVal += ", Version : 0x" + Integer.toHexString(mAppVersion); return retVal; } diff --git a/core/java/android/os/WorkSource.java b/core/java/android/os/WorkSource.java index ecec448aba4c..8632aad87dac 100644 --- a/core/java/android/os/WorkSource.java +++ b/core/java/android/os/WorkSource.java @@ -1,10 +1,13 @@ package android.os; +import android.annotation.Nullable; import android.os.WorkSourceProto; import android.util.Log; import android.util.proto.ProtoOutputStream; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Objects; /** * Describes the source of some work that may be done by someone else. @@ -19,6 +22,8 @@ public class WorkSource implements Parcelable { int[] mUids; String[] mNames; + private ArrayList<WorkChain> mChains; + /** * Internal statics to avoid object allocations in some operations. * The WorkSource object itself is not thread safe, but we need to @@ -39,6 +44,7 @@ public class WorkSource implements Parcelable { */ public WorkSource() { mNum = 0; + mChains = null; } /** @@ -48,6 +54,7 @@ public class WorkSource implements Parcelable { public WorkSource(WorkSource orig) { if (orig == null) { mNum = 0; + mChains = null; return; } mNum = orig.mNum; @@ -58,6 +65,16 @@ public class WorkSource implements Parcelable { mUids = null; mNames = null; } + + if (orig.mChains != null) { + // Make a copy of all WorkChains that exist on |orig| since they are mutable. + mChains = new ArrayList<>(orig.mChains.size()); + for (WorkChain chain : orig.mChains) { + mChains.add(new WorkChain(chain)); + } + } else { + mChains = null; + } } /** @hide */ @@ -65,6 +82,7 @@ public class WorkSource implements Parcelable { mNum = 1; mUids = new int[] { uid, 0 }; mNames = null; + mChains = null; } /** @hide */ @@ -75,12 +93,21 @@ public class WorkSource implements Parcelable { mNum = 1; mUids = new int[] { uid, 0 }; mNames = new String[] { name, null }; + mChains = null; } WorkSource(Parcel in) { mNum = in.readInt(); mUids = in.createIntArray(); mNames = in.createStringArray(); + + int numChains = in.readInt(); + if (numChains > 0) { + mChains = new ArrayList<>(numChains); + in.readParcelableList(mChains, WorkChain.class.getClassLoader()); + } else { + mChains = null; + } } /** @hide */ @@ -99,7 +126,8 @@ public class WorkSource implements Parcelable { } /** - * Clear names from this WorkSource. Uids are left intact. + * Clear names from this WorkSource. Uids are left intact. WorkChains if any, are left + * intact. * * <p>Useful when combining with another WorkSource that doesn't have names. * @hide @@ -127,11 +155,16 @@ public class WorkSource implements Parcelable { */ public void clear() { mNum = 0; + if (mChains != null) { + mChains.clear(); + } } @Override public boolean equals(Object o) { - return o instanceof WorkSource && !diff((WorkSource)o); + return o instanceof WorkSource + && !diff((WorkSource) o) + && Objects.equals(mChains, ((WorkSource) o).mChains); } @Override @@ -145,6 +178,11 @@ public class WorkSource implements Parcelable { result = ((result << 4) | (result >>> 28)) ^ mNames[i].hashCode(); } } + + if (mChains != null) { + result = ((result << 4) | (result >>> 28)) ^ mChains.hashCode(); + } + return result; } @@ -153,6 +191,8 @@ public class WorkSource implements Parcelable { * @param other The WorkSource to compare against. * @return If there is a difference, true is returned. */ + // TODO: This is a public API so it cannot be renamed. Because it is used in several places, + // we keep its semantics the same and ignore any differences in WorkChains (if any). public boolean diff(WorkSource other) { int N = mNum; if (N != other.mNum) { @@ -175,12 +215,15 @@ public class WorkSource implements Parcelable { /** * Replace the current contents of this work source with the given - * work source. If <var>other</var> is null, the current work source + * work source. If {@code other} is null, the current work source * will be made empty. */ public void set(WorkSource other) { if (other == null) { mNum = 0; + if (mChains != null) { + mChains.clear(); + } return; } mNum = other.mNum; @@ -203,6 +246,18 @@ public class WorkSource implements Parcelable { mUids = null; mNames = null; } + + if (other.mChains != null) { + if (mChains != null) { + mChains.clear(); + } else { + mChains = new ArrayList<>(other.mChains.size()); + } + + for (WorkChain chain : other.mChains) { + mChains.add(new WorkChain(chain)); + } + } } /** @hide */ @@ -211,6 +266,7 @@ public class WorkSource implements Parcelable { if (mUids == null) mUids = new int[2]; mUids[0] = uid; mNames = null; + mChains.clear(); } /** @hide */ @@ -225,9 +281,21 @@ public class WorkSource implements Parcelable { } mUids[0] = uid; mNames[0] = name; + mChains.clear(); } - /** @hide */ + /** + * Legacy API, DO NOT USE: Only deals with flat UIDs and tags. No chains are transferred, and no + * differences in chains are returned. This will be removed once its callers have been + * rewritten. + * + * NOTE: This is currently only used in GnssLocationProvider. + * + * @hide + * @deprecated for internal use only. WorkSources are opaque and no external callers should need + * to be aware of internal differences. + */ + @Deprecated public WorkSource[] setReturningDiffs(WorkSource other) { synchronized (sTmpWorkSource) { sNewbWork = null; @@ -251,11 +319,34 @@ public class WorkSource implements Parcelable { */ public boolean add(WorkSource other) { synchronized (sTmpWorkSource) { - return updateLocked(other, false, false); + boolean uidAdded = updateLocked(other, false, false); + + boolean chainAdded = false; + if (other.mChains != null) { + // NOTE: This is quite an expensive operation, especially if the number of chains + // is large. We could look into optimizing it if it proves problematic. + if (mChains == null) { + mChains = new ArrayList<>(other.mChains.size()); + } + + for (WorkChain wc : other.mChains) { + if (!mChains.contains(wc)) { + mChains.add(new WorkChain(wc)); + } + } + } + + return uidAdded || chainAdded; } } - /** @hide */ + /** + * Legacy API: DO NOT USE. Only in use from unit tests. + * + * @hide + * @deprecated meant for unit testing use only. Will be removed in a future API revision. + */ + @Deprecated public WorkSource addReturningNewbs(WorkSource other) { synchronized (sTmpWorkSource) { sNewbWork = null; @@ -311,22 +402,14 @@ public class WorkSource implements Parcelable { return true; } - /** @hide */ - public WorkSource addReturningNewbs(int uid) { - synchronized (sTmpWorkSource) { - sNewbWork = null; - sTmpWorkSource.mUids[0] = uid; - updateLocked(sTmpWorkSource, false, true); - return sNewbWork; - } - } - public boolean remove(WorkSource other) { if (mNum <= 0 || other.mNum <= 0) { return false; } + + boolean uidRemoved = false; if (mNames == null && other.mNames == null) { - return removeUids(other); + uidRemoved = removeUids(other); } else { if (mNames == null) { throw new IllegalArgumentException("Other " + other + " has names, but target " @@ -336,24 +419,44 @@ public class WorkSource implements Parcelable { throw new IllegalArgumentException("Target " + this + " has names, but other " + other + " does not"); } - return removeUidsAndNames(other); + uidRemoved = removeUidsAndNames(other); } - } - /** @hide */ - public WorkSource stripNames() { - if (mNum <= 0) { - return new WorkSource(); - } - WorkSource result = new WorkSource(); - int lastUid = -1; - for (int i=0; i<mNum; i++) { - int uid = mUids[i]; - if (i == 0 || lastUid != uid) { - result.add(uid); + boolean chainRemoved = false; + if (other.mChains != null) { + if (mChains != null) { + chainRemoved = mChains.removeAll(other.mChains); } + } else if (mChains != null) { + mChains.clear(); + chainRemoved = true; } - return result; + + return uidRemoved || chainRemoved; + } + + /** + * Create a new {@code WorkChain} associated with this WorkSource and return it. + * + * @hide + */ + public WorkChain createWorkChain() { + if (mChains == null) { + mChains = new ArrayList<>(4); + } + + final WorkChain wc = new WorkChain(); + mChains.add(wc); + + return wc; + } + + /** + * @return the list of {@code WorkChains} associated with this {@code WorkSource}. + * @hide + */ + public ArrayList<WorkChain> getWorkChains() { + return mChains; } private boolean removeUids(WorkSource other) { @@ -664,6 +767,167 @@ public class WorkSource implements Parcelable { } } + /** + * Represents an attribution chain for an item of work being performed. An attribution chain is + * an indexed list of {code (uid, tag)} nodes. The node at {@code index == 0} is the initiator + * of the work, and the node at the highest index performs the work. Nodes at other indices + * are intermediaries that facilitate the work. Examples : + * + * (1) Work being performed by uid=2456 (no chaining): + * <pre> + * WorkChain { + * mUids = { 2456 } + * mTags = { null } + * mSize = 1; + * } + * </pre> + * + * (2) Work being performed by uid=2456 (from component "c1") on behalf of uid=5678: + * + * <pre> + * WorkChain { + * mUids = { 5678, 2456 } + * mTags = { null, "c1" } + * mSize = 1 + * } + * </pre> + * + * Attribution chains are mutable, though the only operation that can be performed on them + * is the addition of a new node at the end of the attribution chain to represent + * + * @hide + */ + public static class WorkChain implements Parcelable { + private int mSize; + private int[] mUids; + private String[] mTags; + + // @VisibleForTesting + public WorkChain() { + mSize = 0; + mUids = new int[4]; + mTags = new String[4]; + } + + // @VisibleForTesting + public WorkChain(WorkChain other) { + mSize = other.mSize; + mUids = other.mUids.clone(); + mTags = other.mTags.clone(); + } + + private WorkChain(Parcel in) { + mSize = in.readInt(); + mUids = in.createIntArray(); + mTags = in.createStringArray(); + } + + /** + * Append a node whose uid is {@code uid} and whose optional tag is {@code tag} to this + * {@code WorkChain}. + */ + public WorkChain addNode(int uid, @Nullable String tag) { + if (mSize == mUids.length) { + resizeArrays(); + } + + mUids[mSize] = uid; + mTags[mSize] = tag; + mSize++; + + return this; + } + + // TODO: The following three trivial getters are purely for testing and will be removed + // once we have higher level logic in place, e.g for serializing this WorkChain to a proto, + // diffing it etc. + // + // @VisibleForTesting + public int[] getUids() { + return mUids; + } + // @VisibleForTesting + public String[] getTags() { + return mTags; + } + // @VisibleForTesting + public int getSize() { + return mSize; + } + + private void resizeArrays() { + final int newSize = mSize * 2; + int[] uids = new int[newSize]; + String[] tags = new String[newSize]; + + System.arraycopy(mUids, 0, uids, 0, mSize); + System.arraycopy(mTags, 0, tags, 0, mSize); + + mUids = uids; + mTags = tags; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder("WorkChain{"); + for (int i = 0; i < mSize; ++i) { + if (i != 0) { + result.append(", "); + } + result.append("("); + result.append(mUids[i]); + if (mTags[i] != null) { + result.append(", "); + result.append(mTags[i]); + } + result.append(")"); + } + + result.append("}"); + return result.toString(); + } + + @Override + public int hashCode() { + return (mSize + 31 * Arrays.hashCode(mUids)) * 31 + Arrays.hashCode(mTags); + } + + @Override + public boolean equals(Object o) { + if (o instanceof WorkChain) { + WorkChain other = (WorkChain) o; + + return mSize == other.mSize + && Arrays.equals(mUids, other.mUids) + && Arrays.equals(mTags, other.mTags); + } + + return false; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mSize); + dest.writeIntArray(mUids); + dest.writeStringArray(mTags); + } + + public static final Parcelable.Creator<WorkChain> CREATOR = + new Parcelable.Creator<WorkChain>() { + public WorkChain createFromParcel(Parcel in) { + return new WorkChain(in); + } + public WorkChain[] newArray(int size) { + return new WorkChain[size]; + } + }; + } + @Override public int describeContents() { return 0; @@ -674,6 +938,13 @@ public class WorkSource implements Parcelable { dest.writeInt(mNum); dest.writeIntArray(mUids); dest.writeStringArray(mNames); + + if (mChains == null) { + dest.writeInt(-1); + } else { + dest.writeInt(mChains.size()); + dest.writeParcelableList(mChains, flags); + } } @Override @@ -690,6 +961,17 @@ public class WorkSource implements Parcelable { result.append(mNames[i]); } } + + if (mChains != null) { + result.append(" chains="); + for (int i = 0; i < mChains.size(); ++i) { + if (i != 0) { + result.append(", "); + } + result.append(mChains.get(i)); + } + } + result.append("}"); return result.toString(); } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 71e039a1e0ef..6bca37af376a 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -42,8 +42,7 @@ import java.lang.ref.WeakReference; * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) * Canvas.drawText()} directly.</p> */ -public class DynamicLayout extends Layout -{ +public class DynamicLayout extends Layout { private static final int PRIORITY = 128; private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400; @@ -303,8 +302,9 @@ public class DynamicLayout extends Layout } /** - * Make a layout for the specified text that will be updated as the text is changed. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DynamicLayout(@NonNull CharSequence base, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @@ -315,9 +315,9 @@ public class DynamicLayout extends Layout } /** - * Make a layout for the transformed text (password transformation being the primary example of - * a transformation) that will be updated as the base text is changed. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @@ -328,10 +328,9 @@ public class DynamicLayout extends Layout } /** - * Make a layout for the transformed text (password transformation being the primary example of - * a transformation) that will be updated as the base text is changed. If ellipsize is non-null, - * the Layout will ellipsize the text down to ellipsizedWidth. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, @NonNull TextPaint paint, @IntRange(from = 0) int width, @NonNull Alignment align, @@ -351,7 +350,9 @@ public class DynamicLayout extends Layout * the Layout will ellipsize the text down to ellipsizedWidth. * * @hide + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DynamicLayout(@NonNull CharSequence base, @NonNull CharSequence display, @NonNull TextPaint paint, @IntRange(from = 0) int width, diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index 2e10fe8d4267..d69b1190140f 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -454,6 +454,10 @@ public class StaticLayout extends Layout { private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3); } + /** + * @deprecated Use {@link Builder} instead. + */ + @Deprecated public StaticLayout(CharSequence source, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, @@ -463,16 +467,9 @@ public class StaticLayout extends Layout { } /** - * @hide + * @deprecated Use {@link Builder} instead. */ - public StaticLayout(CharSequence source, TextPaint paint, - int width, Alignment align, TextDirectionHeuristic textDir, - float spacingmult, float spacingadd, - boolean includepad) { - this(source, 0, source.length(), paint, width, align, textDir, - spacingmult, spacingadd, includepad); - } - + @Deprecated public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, @@ -483,17 +480,9 @@ public class StaticLayout extends Layout { } /** - * @hide + * @deprecated Use {@link Builder} instead. */ - public StaticLayout(CharSequence source, int bufstart, int bufend, - TextPaint paint, int outerwidth, - Alignment align, TextDirectionHeuristic textDir, - float spacingmult, float spacingadd, - boolean includepad) { - this(source, bufstart, bufend, paint, outerwidth, align, textDir, - spacingmult, spacingadd, includepad, null, 0, Integer.MAX_VALUE); -} - + @Deprecated public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, @@ -507,7 +496,9 @@ public class StaticLayout extends Layout { /** * @hide + * @deprecated Use {@link Builder} instead. */ + @Deprecated public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, TextDirectionHeuristic textDir, @@ -565,6 +556,9 @@ public class StaticLayout extends Layout { Builder.recycle(b); } + /** + * Used by DynamicLayout. + */ /* package */ StaticLayout(@Nullable CharSequence text) { super(text, null, 0, null, 0, 0); diff --git a/core/java/android/view/DisplayCutout.java b/core/java/android/view/DisplayCutout.java index 19cd42e1fa18..e448f14ca97d 100644 --- a/core/java/android/view/DisplayCutout.java +++ b/core/java/android/view/DisplayCutout.java @@ -21,40 +21,37 @@ import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; -import android.annotation.NonNull; +import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; import android.os.Parcel; import android.os.Parcelable; import com.android.internal.annotations.VisibleForTesting; -import java.util.ArrayList; import java.util.List; /** * Represents a part of the display that is not functional for displaying content. * * <p>{@code DisplayCutout} is immutable. - * - * @hide will become API */ public final class DisplayCutout { - private static final Rect ZERO_RECT = new Rect(0, 0, 0, 0); - private static final ArrayList<Point> EMPTY_LIST = new ArrayList<>(); + private static final Rect ZERO_RECT = new Rect(); + private static final Region EMPTY_REGION = new Region(); /** - * An instance where {@link #hasCutout()} returns {@code false}. + * An instance where {@link #isEmpty()} returns {@code true}. * * @hide */ - public static final DisplayCutout NO_CUTOUT = - new DisplayCutout(ZERO_RECT, ZERO_RECT, EMPTY_LIST); + public static final DisplayCutout NO_CUTOUT = new DisplayCutout(ZERO_RECT, EMPTY_REGION); private final Rect mSafeInsets; - private final Rect mBoundingRect; - private final List<Point> mBoundingPolygon; + private final Region mBounds; /** * Creates a DisplayCutout instance. @@ -64,22 +61,18 @@ public final class DisplayCutout { * @hide */ @VisibleForTesting - public DisplayCutout(Rect safeInsets, Rect boundingRect, List<Point> boundingPolygon) { + public DisplayCutout(Rect safeInsets, Region bounds) { mSafeInsets = safeInsets != null ? safeInsets : ZERO_RECT; - mBoundingRect = boundingRect != null ? boundingRect : ZERO_RECT; - mBoundingPolygon = boundingPolygon != null ? boundingPolygon : EMPTY_LIST; + mBounds = bounds != null ? bounds : Region.obtain(); } /** - * Returns whether there is a cutout. - * - * If false, the safe insets will all return zero, and the bounding box or polygon will be - * empty or outside the content view. + * Returns true if there is no cutout or it is outside of the content view. * - * @return {@code true} if there is a cutout, {@code false} otherwise + * @hide */ - public boolean hasCutout() { - return !mSafeInsets.equals(ZERO_RECT); + public boolean isEmpty() { + return mSafeInsets.equals(ZERO_RECT); } /** Returns the inset from the top which avoids the display cutout. */ @@ -103,44 +96,41 @@ public final class DisplayCutout { } /** - * Obtains the safe insets in a rect. + * Returns the safe insets in a rect. * - * @param out a rect which is set to the safe insets. + * @return a rect which is set to the safe insets. * @hide */ - public void getSafeInsets(@NonNull Rect out) { - out.set(mSafeInsets); + public Rect getSafeInsets() { + return new Rect(mSafeInsets); } /** - * Obtains the bounding rect of the cutout. + * Returns the bounding region of the cutout. * - * @param outRect is filled with the bounding rect of the cutout. Coordinates are relative + * @return the bounding region of the cutout. Coordinates are relative * to the top-left corner of the content view. */ - public void getBoundingRect(@NonNull Rect outRect) { - outRect.set(mBoundingRect); + public Region getBounds() { + return Region.obtain(mBounds); } /** - * Obtains the bounding polygon of the cutout. + * Returns the bounding rect of the cutout. * - * @param outPolygon is filled with a list of points representing the corners of a convex - * polygon which covers the cutout. Coordinates are relative to the - * top-left corner of the content view. + * @return the bounding rect of the cutout. Coordinates are relative + * to the top-left corner of the content view. + * @hide */ - public void getBoundingPolygon(List<Point> outPolygon) { - outPolygon.clear(); - for (int i = 0; i < mBoundingPolygon.size(); i++) { - outPolygon.add(new Point(mBoundingPolygon.get(i))); - } + public Rect getBoundingRect() { + // TODO(roosa): Inline. + return mBounds.getBounds(); } @Override public int hashCode() { int result = mSafeInsets.hashCode(); - result = result * 31 + mBoundingRect.hashCode(); - result = result * 31 + mBoundingPolygon.hashCode(); + result = result * 31 + mBounds.getBounds().hashCode(); return result; } @@ -152,8 +142,7 @@ public final class DisplayCutout { if (o instanceof DisplayCutout) { DisplayCutout c = (DisplayCutout) o; return mSafeInsets.equals(c.mSafeInsets) - && mBoundingRect.equals(c.mBoundingRect) - && mBoundingPolygon.equals(c.mBoundingPolygon); + && mBounds.equals(c.mBounds); } return false; } @@ -161,7 +150,7 @@ public final class DisplayCutout { @Override public String toString() { return "DisplayCutout{insets=" + mSafeInsets - + " bounding=" + mBoundingRect + + " bounds=" + mBounds + "}"; } @@ -172,15 +161,13 @@ public final class DisplayCutout { * @hide */ public DisplayCutout inset(int insetLeft, int insetTop, int insetRight, int insetBottom) { - if (mBoundingRect.isEmpty() + if (mBounds.isEmpty() || insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) { return this; } Rect safeInsets = new Rect(mSafeInsets); - Rect boundingRect = new Rect(mBoundingRect); - ArrayList<Point> boundingPolygon = new ArrayList<>(); - getBoundingPolygon(boundingPolygon); + Region bounds = Region.obtain(mBounds); // Note: it's not really well defined what happens when the inset is negative, because we // don't know if the safe inset needs to expand in general. @@ -197,10 +184,9 @@ public final class DisplayCutout { safeInsets.right = atLeastZero(safeInsets.right - insetRight); } - boundingRect.offset(-insetLeft, -insetTop); - offset(boundingPolygon, -insetLeft, -insetTop); + bounds.translate(-insetLeft, -insetTop); - return new DisplayCutout(safeInsets, boundingRect, boundingPolygon); + return new DisplayCutout(safeInsets, bounds); } /** @@ -210,20 +196,17 @@ public final class DisplayCutout { * @hide */ public DisplayCutout calculateRelativeTo(Rect frame) { - if (mBoundingRect.isEmpty() || !Rect.intersects(frame, mBoundingRect)) { + if (mBounds.isEmpty() || !Rect.intersects(frame, mBounds.getBounds())) { return NO_CUTOUT; } - Rect boundingRect = new Rect(mBoundingRect); - ArrayList<Point> boundingPolygon = new ArrayList<>(); - getBoundingPolygon(boundingPolygon); - - return DisplayCutout.calculateRelativeTo(frame, boundingRect, boundingPolygon); + return DisplayCutout.calculateRelativeTo(frame, Region.obtain(mBounds)); } - private static DisplayCutout calculateRelativeTo(Rect frame, Rect boundingRect, - ArrayList<Point> boundingPolygon) { + private static DisplayCutout calculateRelativeTo(Rect frame, Region bounds) { + Rect boundingRect = bounds.getBounds(); Rect safeRect = new Rect(); + int bestArea = 0; int bestVariant = 0; for (int variant = ROTATION_0; variant <= ROTATION_270; variant++) { @@ -247,10 +230,9 @@ public final class DisplayCutout { Math.max(0, frame.bottom - safeRect.bottom)); } - boundingRect.offset(-frame.left, -frame.top); - offset(boundingPolygon, -frame.left, -frame.top); + bounds.translate(-frame.left, -frame.top); - return new DisplayCutout(safeRect, boundingRect, boundingPolygon); + return new DisplayCutout(safeRect, bounds); } private static int calculateInsetVariantArea(Rect frame, Rect boundingRect, int variant, @@ -277,11 +259,6 @@ public final class DisplayCutout { return value < 0 ? 0 : value; } - private static void offset(ArrayList<Point> points, int dx, int dy) { - for (int i = 0; i < points.size(); i++) { - points.get(i).offset(dx, dy); - } - } /** * Creates an instance from a bounding polygon. @@ -289,20 +266,28 @@ public final class DisplayCutout { * @hide */ public static DisplayCutout fromBoundingPolygon(List<Point> points) { - Rect boundingRect = new Rect(Integer.MAX_VALUE, Integer.MAX_VALUE, - Integer.MIN_VALUE, Integer.MIN_VALUE); - ArrayList<Point> boundingPolygon = new ArrayList<>(); + Region bounds = Region.obtain(); + Path path = new Path(); + path.reset(); for (int i = 0; i < points.size(); i++) { Point point = points.get(i); - boundingRect.left = Math.min(boundingRect.left, point.x); - boundingRect.right = Math.max(boundingRect.right, point.x); - boundingRect.top = Math.min(boundingRect.top, point.y); - boundingRect.bottom = Math.max(boundingRect.bottom, point.y); - boundingPolygon.add(new Point(point)); + if (i == 0) { + path.moveTo(point.x, point.y); + } else { + path.lineTo(point.x, point.y); + } } + path.close(); + + RectF clipRect = new RectF(); + path.computeBounds(clipRect, false /* unused */); + Region clipRegion = Region.obtain(); + clipRegion.set((int) clipRect.left, (int) clipRect.top, + (int) clipRect.right, (int) clipRect.bottom); - return new DisplayCutout(ZERO_RECT, boundingRect, boundingPolygon); + bounds.setPath(path, clipRegion); + return new DisplayCutout(ZERO_RECT, bounds); } /** @@ -336,8 +321,7 @@ public final class DisplayCutout { } else { out.writeInt(1); out.writeTypedObject(mInner.mSafeInsets, flags); - out.writeTypedObject(mInner.mBoundingRect, flags); - out.writeTypedList(mInner.mBoundingPolygon, flags); + out.writeTypedObject(mInner.mBounds, flags); } } @@ -368,13 +352,10 @@ public final class DisplayCutout { return NO_CUTOUT; } - ArrayList<Point> boundingPolygon = new ArrayList<>(); - Rect safeInsets = in.readTypedObject(Rect.CREATOR); - Rect boundingRect = in.readTypedObject(Rect.CREATOR); - in.readTypedList(boundingPolygon, Point.CREATOR); + Region bounds = in.readTypedObject(Region.CREATOR); - return new DisplayCutout(safeInsets, boundingRect, boundingPolygon); + return new DisplayCutout(safeInsets, bounds); } public DisplayCutout get() { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 2c82ac49c84e..6c5091c28708 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1602,7 +1602,7 @@ public final class ViewRootImpl implements ViewParent, if (!layoutInCutout) { // Window is either not laid out in cutout or the status bar inset takes care of // clearing the cutout, so we don't need to dispatch the cutout to the hierarchy. - insets = insets.consumeCutout(); + insets = insets.consumeDisplayCutout(); } host.dispatchApplyWindowInsets(insets); } diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java index df124ac5be28..e5cbe96b9173 100644 --- a/core/java/android/view/WindowInsets.java +++ b/core/java/android/view/WindowInsets.java @@ -17,7 +17,7 @@ package android.view; -import android.annotation.NonNull; +import android.annotation.Nullable; import android.graphics.Rect; /** @@ -49,7 +49,7 @@ public final class WindowInsets { private boolean mSystemWindowInsetsConsumed = false; private boolean mWindowDecorInsetsConsumed = false; private boolean mStableInsetsConsumed = false; - private boolean mCutoutConsumed = false; + private boolean mDisplayCutoutConsumed = false; private static final Rect EMPTY_RECT = new Rect(0, 0, 0, 0); @@ -80,8 +80,9 @@ public final class WindowInsets { mIsRound = isRound; mAlwaysConsumeNavBar = alwaysConsumeNavBar; - mCutoutConsumed = displayCutout == null; - mDisplayCutout = mCutoutConsumed ? DisplayCutout.NO_CUTOUT : displayCutout; + mDisplayCutoutConsumed = displayCutout == null; + mDisplayCutout = (mDisplayCutoutConsumed || displayCutout.isEmpty()) + ? null : displayCutout; } /** @@ -99,7 +100,7 @@ public final class WindowInsets { mIsRound = src.mIsRound; mAlwaysConsumeNavBar = src.mAlwaysConsumeNavBar; mDisplayCutout = src.mDisplayCutout; - mCutoutConsumed = src.mCutoutConsumed; + mDisplayCutoutConsumed = src.mDisplayCutoutConsumed; } /** @hide */ @@ -269,15 +270,16 @@ public final class WindowInsets { */ public boolean hasInsets() { return hasSystemWindowInsets() || hasWindowDecorInsets() || hasStableInsets() - || mDisplayCutout.hasCutout(); + || mDisplayCutout != null; } /** - * @return the display cutout + * Returns the display cutout if there is one. + * + * @return the display cutout or null if there is none * @see DisplayCutout - * @hide pending API */ - @NonNull + @Nullable public DisplayCutout getDisplayCutout() { return mDisplayCutout; } @@ -286,12 +288,11 @@ public final class WindowInsets { * Returns a copy of this WindowInsets with the cutout fully consumed. * * @return A modified copy of this WindowInsets - * @hide pending API */ - public WindowInsets consumeCutout() { + public WindowInsets consumeDisplayCutout() { final WindowInsets result = new WindowInsets(this); - result.mDisplayCutout = DisplayCutout.NO_CUTOUT; - result.mCutoutConsumed = true; + result.mDisplayCutout = null; + result.mDisplayCutoutConsumed = true; return result; } @@ -311,7 +312,7 @@ public final class WindowInsets { */ public boolean isConsumed() { return mSystemWindowInsetsConsumed && mWindowDecorInsetsConsumed && mStableInsetsConsumed - && mCutoutConsumed; + && mDisplayCutoutConsumed; } /** @@ -530,7 +531,7 @@ public final class WindowInsets { return "WindowInsets{systemWindowInsets=" + mSystemWindowInsets + " windowDecorInsets=" + mWindowDecorInsets + " stableInsets=" + mStableInsets - + (mDisplayCutout.hasCutout() ? " cutout=" + mDisplayCutout : "") + + (mDisplayCutout != null ? " cutout=" + mDisplayCutout : "") + (isRound() ? " round" : "") + "}"; } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 012e86406579..cbe012af0b21 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -1286,7 +1286,6 @@ public interface WindowManager extends ViewManager { * The window must correctly position its contents to take the display cutout into account. * * @see DisplayCutout - * @hide for now */ public static final long FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA = 0x00000001; @@ -1294,7 +1293,6 @@ public interface WindowManager extends ViewManager { * Various behavioral options/flags. Default is none. * * @see #FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA - * @hide for now */ @Flags2 public long flags2; @@ -2249,6 +2247,7 @@ public interface WindowManager extends ViewManager { out.writeInt(y); out.writeInt(type); out.writeInt(flags); + out.writeLong(flags2); out.writeInt(privateFlags); out.writeInt(softInputMode); out.writeInt(gravity); @@ -2304,6 +2303,7 @@ public interface WindowManager extends ViewManager { y = in.readInt(); type = in.readInt(); flags = in.readInt(); + flags2 = in.readLong(); privateFlags = in.readInt(); softInputMode = in.readInt(); gravity = in.readInt(); @@ -2436,6 +2436,10 @@ public interface WindowManager extends ViewManager { flags = o.flags; changes |= FLAGS_CHANGED; } + if (flags2 != o.flags2) { + flags2 = o.flags2; + changes |= FLAGS_CHANGED; + } if (privateFlags != o.privateFlags) { privateFlags = o.privateFlags; changes |= PRIVATE_FLAGS_CHANGED; @@ -2689,6 +2693,11 @@ public interface WindowManager extends ViewManager { sb.append(System.lineSeparator()); sb.append(prefix).append(" fl=").append( ViewDebug.flagsToString(LayoutParams.class, "flags", flags)); + if (flags2 != 0) { + sb.append(System.lineSeparator()); + // TODO(roosa): add a long overload for ViewDebug.flagsToString. + sb.append(prefix).append(" fl2=0x").append(Long.toHexString(flags2)); + } if (privateFlags != 0) { sb.append(System.lineSeparator()); sb.append(prefix).append(" pfl=").append(ViewDebug.flagsToString( diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 9ac443b43701..1e17f34af2a6 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -4867,6 +4867,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * Sets line spacing for this TextView. Each line other than the last line will have its height * multiplied by {@code mult} and have {@code add} added to it. * + * @param add The value in pixels that should be added to each line other than the last line. + * This will be applied after the multiplier + * @param mult The value by which each line height other than the last line will be multiplied + * by * * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 8df0fb891f1c..297bae25c3f3 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -123,6 +123,7 @@ cc_library_shared { "android_graphics_Picture.cpp", "android/graphics/Bitmap.cpp", "android/graphics/BitmapFactory.cpp", + "android/graphics/ByteBufferStreamAdaptor.cpp", "android/graphics/Camera.cpp", "android/graphics/CanvasProperty.cpp", "android/graphics/ColorFilter.cpp", @@ -134,6 +135,7 @@ cc_library_shared { "android/graphics/GraphicBuffer.cpp", "android/graphics/Graphics.cpp", "android/graphics/HarfBuzzNGFaceSkia.cpp", + "android/graphics/ImageDecoder.cpp", "android/graphics/Interpolator.cpp", "android/graphics/MaskFilter.cpp", "android/graphics/Matrix.cpp", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 9e907bdf458f..b5d18683403f 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -57,10 +57,12 @@ extern int register_android_os_Process(JNIEnv* env); extern int register_android_graphics_Bitmap(JNIEnv*); extern int register_android_graphics_BitmapFactory(JNIEnv*); extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*); +extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env); extern int register_android_graphics_Camera(JNIEnv* env); extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env); extern int register_android_graphics_GraphicBuffer(JNIEnv* env); extern int register_android_graphics_Graphics(JNIEnv* env); +extern int register_android_graphics_ImageDecoder(JNIEnv*); extern int register_android_graphics_Interpolator(JNIEnv* env); extern int register_android_graphics_MaskFilter(JNIEnv* env); extern int register_android_graphics_Movie(JNIEnv* env); @@ -644,6 +646,7 @@ int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote) char methodTraceFileBuf[sizeof("-Xmethod-trace-file:") + PROPERTY_VALUE_MAX]; char methodTraceFileSizeBuf[sizeof("-Xmethod-trace-file-size:") + PROPERTY_VALUE_MAX]; std::string fingerprintBuf; + char jdwpProviderBuf[sizeof("-XjdwpProvider:") - 1 + PROPERTY_VALUE_MAX]; bool checkJni = false; property_get("dalvik.vm.checkjni", propBuf, ""); @@ -766,9 +769,15 @@ int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote) * Set suspend=y to pause during VM init and use android ADB transport. */ if (zygote) { - addOption("-agentlib:jdwp=transport=dt_android_adb,suspend=n,server=y"); + addOption("-XjdwpOptions:suspend=n,server=y"); } + // Set the JDWP provider. By default let the runtime choose. + parseRuntimeOption("dalvik.vm.jdwp-provider", + jdwpProviderBuf, + "-XjdwpProvider:", + "default"); + parseRuntimeOption("dalvik.vm.lockprof.threshold", lockProfThresholdBuf, "-Xlockprofthreshold:"); @@ -1380,6 +1389,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_Bitmap), REG_JNI(register_android_graphics_BitmapFactory), REG_JNI(register_android_graphics_BitmapRegionDecoder), + REG_JNI(register_android_graphics_ByteBufferStreamAdaptor), REG_JNI(register_android_graphics_Camera), REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor), REG_JNI(register_android_graphics_CanvasProperty), @@ -1387,6 +1397,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_DrawFilter), REG_JNI(register_android_graphics_FontFamily), REG_JNI(register_android_graphics_GraphicBuffer), + REG_JNI(register_android_graphics_ImageDecoder), REG_JNI(register_android_graphics_Interpolator), REG_JNI(register_android_graphics_MaskFilter), REG_JNI(register_android_graphics_Matrix), diff --git a/core/jni/android/graphics/BitmapFactory.cpp b/core/jni/android/graphics/BitmapFactory.cpp index 2e8c27ab7217..79c5a340f16f 100644 --- a/core/jni/android/graphics/BitmapFactory.cpp +++ b/core/jni/android/graphics/BitmapFactory.cpp @@ -47,9 +47,6 @@ jfieldID gOptions_bitmapFieldID; jfieldID gBitmap_ninePatchInsetsFieldID; -jclass gInsetStruct_class; -jmethodID gInsetStruct_constructorMethodID; - jclass gBitmapConfig_class; jmethodID gBitmapConfig_nativeToConfigMethodID; @@ -99,43 +96,6 @@ jstring encodedFormatToString(JNIEnv* env, SkEncodedImageFormat format) { return jstr; } -static void scaleDivRange(int32_t* divs, int count, float scale, int maxValue) { - for (int i = 0; i < count; i++) { - divs[i] = int32_t(divs[i] * scale + 0.5f); - if (i > 0 && divs[i] == divs[i - 1]) { - divs[i]++; // avoid collisions - } - } - - if (CC_UNLIKELY(divs[count - 1] > maxValue)) { - // if the collision avoidance above put some divs outside the bounds of the bitmap, - // slide outer stretchable divs inward to stay within bounds - int highestAvailable = maxValue; - for (int i = count - 1; i >= 0; i--) { - divs[i] = highestAvailable; - if (i > 0 && divs[i] <= divs[i-1]){ - // keep shifting - highestAvailable = divs[i] - 1; - } else { - break; - } - } - } -} - -static void scaleNinePatchChunk(android::Res_png_9patch* chunk, float scale, - int scaledWidth, int scaledHeight) { - chunk->paddingLeft = int(chunk->paddingLeft * scale + 0.5f); - chunk->paddingTop = int(chunk->paddingTop * scale + 0.5f); - chunk->paddingRight = int(chunk->paddingRight * scale + 0.5f); - chunk->paddingBottom = int(chunk->paddingBottom * scale + 0.5f); - - // The max value for the divRange is one pixel less than the actual max to ensure that the size - // of the last div is not zero. A div of size 0 is considered invalid input and will not render. - scaleDivRange(chunk->getXDivs(), chunk->numXDivs, scale, scaledWidth - 1); - scaleDivRange(chunk->getYDivs(), chunk->numYDivs, scale, scaledHeight - 1); -} - class ScaleCheckingAllocator : public SkBitmap::HeapAllocator { public: ScaleCheckingAllocator(float scale, int size) @@ -428,7 +388,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, jbyteArray ninePatchChunk = NULL; if (peeker.mPatch != NULL) { if (willScale) { - scaleNinePatchChunk(peeker.mPatch, scale, scaledWidth, scaledHeight); + peeker.scale(scale, scale, scaledWidth, scaledHeight); } size_t ninePatchArraySize = peeker.mPatch->serializedSize(); @@ -448,12 +408,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, jobject ninePatchInsets = NULL; if (peeker.mHasInsets) { - ninePatchInsets = env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID, - peeker.mOpticalInsets[0], peeker.mOpticalInsets[1], - peeker.mOpticalInsets[2], peeker.mOpticalInsets[3], - peeker.mOutlineInsets[0], peeker.mOutlineInsets[1], - peeker.mOutlineInsets[2], peeker.mOutlineInsets[3], - peeker.mOutlineRadius, peeker.mOutlineAlpha, scale); + ninePatchInsets = peeker.createNinePatchInsets(env, scale); if (ninePatchInsets == NULL) { return nullObjectReturn("nine patch insets == null"); } @@ -508,13 +463,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, } if (padding) { - if (peeker.mPatch != NULL) { - GraphicsJNI::set_jrect(env, padding, - peeker.mPatch->paddingLeft, peeker.mPatch->paddingTop, - peeker.mPatch->paddingRight, peeker.mPatch->paddingBottom); - } else { - GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1); - } + peeker.getPadding(env, padding); } // If we get here, the outputBitmap should have an installed pixelref. @@ -705,11 +654,6 @@ int register_android_graphics_BitmapFactory(JNIEnv* env) { gBitmap_ninePatchInsetsFieldID = GetFieldIDOrDie(env, bitmap_class, "mNinePatchInsets", "Landroid/graphics/NinePatch$InsetStruct;"); - gInsetStruct_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, - "android/graphics/NinePatch$InsetStruct")); - gInsetStruct_constructorMethodID = GetMethodIDOrDie(env, gInsetStruct_class, "<init>", - "(IIIIIIIIFIF)V"); - gBitmapConfig_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap$Config")); gBitmapConfig_nativeToConfigMethodID = GetStaticMethodIDOrDie(env, gBitmapConfig_class, diff --git a/core/jni/android/graphics/ByteBufferStreamAdaptor.cpp b/core/jni/android/graphics/ByteBufferStreamAdaptor.cpp new file mode 100644 index 000000000000..115edd4fe4f4 --- /dev/null +++ b/core/jni/android/graphics/ByteBufferStreamAdaptor.cpp @@ -0,0 +1,323 @@ +#include "ByteBufferStreamAdaptor.h" +#include "core_jni_helpers.h" + +#include <SkStream.h> + +using namespace android; + +static jmethodID gByteBuffer_getMethodID; +static jmethodID gByteBuffer_setPositionMethodID; + +static JNIEnv* get_env_or_die(JavaVM* jvm) { + JNIEnv* env; + if (jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", jvm); + } + return env; +} + +class ByteBufferStream : public SkStreamAsset { +private: + ByteBufferStream(JavaVM* jvm, jobject jbyteBuffer, size_t initialPosition, size_t length, + jbyteArray storage) + : mJvm(jvm) + , mByteBuffer(jbyteBuffer) + , mPosition(0) + , mInitialPosition(initialPosition) + , mLength(length) + , mStorage(storage) {} + +public: + static ByteBufferStream* Create(JavaVM* jvm, JNIEnv* env, jobject jbyteBuffer, + size_t position, size_t length) { + // This object outlives its native method call. + jbyteBuffer = env->NewGlobalRef(jbyteBuffer); + if (!jbyteBuffer) { + return nullptr; + } + + jbyteArray storage = env->NewByteArray(kStorageSize); + if (!storage) { + env->DeleteGlobalRef(jbyteBuffer); + return nullptr; + } + + // This object outlives its native method call. + storage = static_cast<jbyteArray>(env->NewGlobalRef(storage)); + if (!storage) { + env->DeleteGlobalRef(jbyteBuffer); + return nullptr; + } + + return new ByteBufferStream(jvm, jbyteBuffer, position, length, storage); + } + + ~ByteBufferStream() override { + auto* env = get_env_or_die(mJvm); + env->DeleteGlobalRef(mByteBuffer); + env->DeleteGlobalRef(mStorage); + } + + size_t read(void* buffer, size_t size) override { + if (size > mLength - mPosition) { + size = mLength - mPosition; + } + if (!size) { + return 0; + } + + if (!buffer) { + return this->setPosition(mPosition + size); + } + + auto* env = get_env_or_die(mJvm); + size_t bytesRead = 0; + do { + const size_t requested = (size > kStorageSize) ? kStorageSize : size; + const jint jrequested = static_cast<jint>(requested); + env->CallObjectMethod(mByteBuffer, gByteBuffer_getMethodID, mStorage, 0, jrequested); + if (env->ExceptionCheck()) { + ALOGE("Error in ByteBufferStream::read - was the ByteBuffer modified externally?"); + env->ExceptionDescribe(); + env->ExceptionClear(); + mPosition = mLength; + return bytesRead; + } + + env->GetByteArrayRegion(mStorage, 0, requested, reinterpret_cast<jbyte*>(buffer)); + if (env->ExceptionCheck()) { + ALOGE("Internal error in ByteBufferStream::read"); + env->ExceptionDescribe(); + env->ExceptionClear(); + mPosition = mLength; + return bytesRead; + } + + mPosition += requested; + buffer = reinterpret_cast<void*>(reinterpret_cast<char*>(buffer) + requested); + bytesRead += requested; + size -= requested; + } while (size); + return bytesRead; + } + + bool isAtEnd() const override { return mLength == mPosition; } + + // SkStreamRewindable overrides + bool rewind() override { return this->setPosition(0); } + + SkStreamAsset* onDuplicate() const override { + // SkStreamRewindable requires overriding this, but it is not called by + // decoders, so does not need a true implementation. A proper + // implementation would require duplicating the ByteBuffer, which has + // its own internal position state. + return nullptr; + } + + // SkStreamSeekable overrides + size_t getPosition() const override { return mPosition; } + + bool seek(size_t position) override { + return this->setPosition(position > mLength ? mLength : position); + } + + bool move(long offset) override { + long newPosition = mPosition + offset; + if (newPosition < 0) { + return this->setPosition(0); + } + return this->seek(static_cast<size_t>(newPosition)); + } + + SkStreamAsset* onFork() const override { + // SkStreamSeekable requires overriding this, but it is not called by + // decoders, so does not need a true implementation. A proper + // implementation would require duplicating the ByteBuffer, which has + // its own internal position state. + return nullptr; + } + + // SkStreamAsset overrides + size_t getLength() const override { return mLength; } + +private: + JavaVM* mJvm; + jobject mByteBuffer; + // Logical position of the SkStream, between 0 and mLength. + size_t mPosition; + // Initial position of mByteBuffer, treated as mPosition 0. + const size_t mInitialPosition; + // Logical length of the SkStream, from mInitialPosition to + // mByteBuffer.limit(). + const size_t mLength; + + // Range has already been checked by the caller. + bool setPosition(size_t newPosition) { + auto* env = get_env_or_die(mJvm); + env->CallObjectMethod(mByteBuffer, gByteBuffer_setPositionMethodID, + newPosition + mInitialPosition); + if (env->ExceptionCheck()) { + ALOGE("Internal error in ByteBufferStream::setPosition"); + env->ExceptionDescribe(); + env->ExceptionClear(); + mPosition = mLength; + return false; + } + mPosition = newPosition; + return true; + } + + // FIXME: This is an arbitrary storage size, which should be plenty for + // some formats (png, gif, many bmps). But for jpeg, the more we can supply + // in one call the better, and webp really wants all of the data. How to + // best choose the amount of storage used? + static constexpr size_t kStorageSize = 4096; + jbyteArray mStorage; +}; + +class ByteArrayStream : public SkStreamAsset { +private: + ByteArrayStream(JavaVM* jvm, jbyteArray jarray, size_t offset, size_t length) + : mJvm(jvm), mByteArray(jarray), mOffset(offset), mPosition(0), mLength(length) {} + +public: + static ByteArrayStream* Create(JavaVM* jvm, JNIEnv* env, jbyteArray jarray, size_t offset, + size_t length) { + // This object outlives its native method call. + jarray = static_cast<jbyteArray>(env->NewGlobalRef(jarray)); + if (!jarray) { + return nullptr; + } + return new ByteArrayStream(jvm, jarray, offset, length); + } + + ~ByteArrayStream() override { + auto* env = get_env_or_die(mJvm); + env->DeleteGlobalRef(mByteArray); + } + + size_t read(void* buffer, size_t size) override { + if (size > mLength - mPosition) { + size = mLength - mPosition; + } + if (!size) { + return 0; + } + + auto* env = get_env_or_die(mJvm); + if (buffer) { + env->GetByteArrayRegion(mByteArray, mPosition + mOffset, size, + reinterpret_cast<jbyte*>(buffer)); + if (env->ExceptionCheck()) { + ALOGE("Internal error in ByteArrayStream::read"); + env->ExceptionDescribe(); + env->ExceptionClear(); + mPosition = mLength; + return 0; + } + } + + mPosition += size; + return size; + } + + bool isAtEnd() const override { return mLength == mPosition; } + + // SkStreamRewindable overrides + bool rewind() override { + mPosition = 0; + return true; + } + SkStreamAsset* onDuplicate() const override { + // SkStreamRewindable requires overriding this, but it is not called by + // decoders, so does not need a true implementation. Note that a proper + // implementation is fairly straightforward + return nullptr; + } + + // SkStreamSeekable overrides + size_t getPosition() const override { return mPosition; } + + bool seek(size_t position) override { + mPosition = (position > mLength) ? mLength : position; + return true; + } + + bool move(long offset) override { + long newPosition = mPosition + offset; + if (newPosition < 0) { + return this->seek(0); + } + return this->seek(static_cast<size_t>(newPosition)); + } + + SkStreamAsset* onFork() const override { + // SkStreamSeekable requires overriding this, but it is not called by + // decoders, so does not need a true implementation. Note that a proper + // implementation is fairly straightforward + return nullptr; + } + + // SkStreamAsset overrides + size_t getLength() const override { return mLength; } + +private: + JavaVM* mJvm; + jbyteArray mByteArray; + // Offset in mByteArray. Only used when communicating with Java. + const size_t mOffset; + // Logical position of the SkStream, between 0 and mLength. + size_t mPosition; + const size_t mLength; +}; + +struct release_proc_context { + JavaVM* jvm; + jobject jbyteBuffer; +}; + +std::unique_ptr<SkStream> CreateByteBufferStreamAdaptor(JNIEnv* env, jobject jbyteBuffer, + size_t position, size_t limit) { + JavaVM* jvm; + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&jvm) != JNI_OK); + + const size_t length = limit - position; + void* addr = env->GetDirectBufferAddress(jbyteBuffer); + if (addr) { + addr = reinterpret_cast<void*>(reinterpret_cast<char*>(addr) + position); + jbyteBuffer = env->NewGlobalRef(jbyteBuffer); + if (!jbyteBuffer) { + return nullptr; + } + + auto* context = new release_proc_context{jvm, jbyteBuffer}; + auto releaseProc = [](const void*, void* context) { + auto* c = reinterpret_cast<release_proc_context*>(context); + JNIEnv* env = get_env_or_die(c->jvm); + env->DeleteGlobalRef(c->jbyteBuffer); + delete c; + }; + auto data = SkData::MakeWithProc(addr, length, releaseProc, context); + // The new SkMemoryStream will read directly from addr. + return std::unique_ptr<SkStream>(new SkMemoryStream(std::move(data))); + } + + // Non-direct, or direct access is not supported. + return std::unique_ptr<SkStream>(ByteBufferStream::Create(jvm, env, jbyteBuffer, position, + length)); +} + +std::unique_ptr<SkStream> CreateByteArrayStreamAdaptor(JNIEnv* env, jbyteArray array, size_t offset, + size_t length) { + JavaVM* jvm; + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&jvm) != JNI_OK); + + return std::unique_ptr<SkStream>(ByteArrayStream::Create(jvm, env, array, offset, length)); +} + +int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env) { + jclass byteBuffer_class = FindClassOrDie(env, "java/nio/ByteBuffer"); + gByteBuffer_getMethodID = GetMethodIDOrDie(env, byteBuffer_class, "get", "([BII)Ljava/nio/ByteBuffer;"); + gByteBuffer_setPositionMethodID = GetMethodIDOrDie(env, byteBuffer_class, "position", "(I)Ljava/nio/Buffer;"); + return true; +} diff --git a/core/jni/android/graphics/ByteBufferStreamAdaptor.h b/core/jni/android/graphics/ByteBufferStreamAdaptor.h new file mode 100644 index 000000000000..367a48fad9b9 --- /dev/null +++ b/core/jni/android/graphics/ByteBufferStreamAdaptor.h @@ -0,0 +1,37 @@ +#ifndef _ANDROID_GRAPHICS_BYTE_BUFFER_STREAM_ADAPTOR_H_ +#define _ANDROID_GRAPHICS_BYTE_BUFFER_STREAM_ADAPTOR_H_ + +#include <jni.h> +#include <memory> + +class SkStream; + +/** + * Create an adaptor for treating a java.nio.ByteBuffer as an SkStream. + * + * This will special case direct ByteBuffers, but not the case where a byte[] + * can be used directly. For that, use CreateByteArrayStreamAdaptor. + * + * @param jbyteBuffer corresponding to the java ByteBuffer. This method will + * add a global ref. + * @param initialPosition returned by ByteBuffer.position(). Decoding starts + * from here. + * @param limit returned by ByteBuffer.limit(). + * + * Returns null on failure. + */ +std::unique_ptr<SkStream> CreateByteBufferStreamAdaptor(JNIEnv*, jobject jbyteBuffer, + size_t initialPosition, size_t limit); + +/** + * Create an adaptor for treating a Java byte[] as an SkStream. + * + * @param offset into the byte[] of the beginning of the data to use. + * @param length of data to use, starting from offset. + * + * Returns null on failure. + */ +std::unique_ptr<SkStream> CreateByteArrayStreamAdaptor(JNIEnv*, jbyteArray array, size_t offset, + size_t length); + +#endif // _ANDROID_GRAPHICS_BYTE_BUFFER_STREAM_ADAPTOR_H_ diff --git a/core/jni/android/graphics/ImageDecoder.cpp b/core/jni/android/graphics/ImageDecoder.cpp new file mode 100644 index 000000000000..bacab2a304cc --- /dev/null +++ b/core/jni/android/graphics/ImageDecoder.cpp @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2017 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 "Bitmap.h" +#include "ByteBufferStreamAdaptor.h" +#include "GraphicsJNI.h" +#include "NinePatchPeeker.h" +#include "Utils.h" +#include "core_jni_helpers.h" + +#include <hwui/Bitmap.h> +#include <hwui/Canvas.h> + +#include <SkAndroidCodec.h> +#include <SkEncodedImageFormat.h> +#include <SkStream.h> + +#include <androidfw/Asset.h> +#include <jni.h> + +using namespace android; + +static jclass gImageDecoder_class; +static jclass gPoint_class; +static jclass gIncomplete_class; +static jclass gCorrupt_class; +static jclass gCanvas_class; +static jmethodID gImageDecoder_constructorMethodID; +static jmethodID gPoint_constructorMethodID; +static jmethodID gIncomplete_constructorMethodID; +static jmethodID gCorrupt_constructorMethodID; +static jmethodID gCallback_onExceptionMethodID; +static jmethodID gPostProcess_postProcessMethodID; +static jmethodID gCanvas_constructorMethodID; +static jmethodID gCanvas_releaseMethodID; + +struct ImageDecoder { + // These need to stay in sync with ImageDecoder.java's Allocator constants. + enum Allocator { + kDefault_Allocator = 0, + kSoftware_Allocator = 1, + kSharedMemory_Allocator = 2, + kHardware_Allocator = 3, + }; + + // These need to stay in sync with PixelFormat.java's Format constants. + enum PixelFormat { + kUnknown = 0, + kTranslucent = -3, + kOpaque = -1, + }; + + std::unique_ptr<SkAndroidCodec> mCodec; + NinePatchPeeker mPeeker; +}; + +static jobject native_create(JNIEnv* env, std::unique_ptr<SkStream> stream) { + if (!stream.get()) { + return nullObjectReturn("Failed to create a stream"); + } + std::unique_ptr<ImageDecoder> decoder(new ImageDecoder); + decoder->mCodec = SkAndroidCodec::MakeFromStream(std::move(stream), &decoder->mPeeker); + if (!decoder->mCodec.get()) { + // FIXME: Add an error code to SkAndroidCodec::MakeFromStream, like + // SkCodec? Then this can print a more informative error message. + // (Or we can print one from within SkCodec.) + ALOGE("Failed to create an SkCodec"); + return nullptr; + } + + const auto& info = decoder->mCodec->getInfo(); + const int width = info.width(); + const int height = info.height(); + return env->NewObject(gImageDecoder_class, gImageDecoder_constructorMethodID, + reinterpret_cast<jlong>(decoder.release()), width, height); +} + +static jobject ImageDecoder_nCreate(JNIEnv* env, jobject /*clazz*/, jlong assetPtr) { + Asset* asset = reinterpret_cast<Asset*>(assetPtr); + std::unique_ptr<SkStream> stream(new AssetStreamAdaptor(asset)); + return native_create(env, std::move(stream)); +} + +static jobject ImageDecoder_nCreateByteBuffer(JNIEnv* env, jobject /*clazz*/, jobject jbyteBuffer, + jint initialPosition, jint limit) { + std::unique_ptr<SkStream> stream = CreateByteBufferStreamAdaptor(env, jbyteBuffer, + initialPosition, limit); + if (!stream) { + return nullptr; + } + return native_create(env, std::move(stream)); +} + +static jobject ImageDecoder_nCreateByteArray(JNIEnv* env, jobject /*clazz*/, jbyteArray byteArray, + jint offset, jint length) { + std::unique_ptr<SkStream> stream(CreateByteArrayStreamAdaptor(env, byteArray, offset, length)); + return native_create(env, std::move(stream)); +} + +static bool supports_any_down_scale(const SkAndroidCodec* codec) { + return codec->getEncodedFormat() == SkEncodedImageFormat::kWEBP; +} + +static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, + jobject jcallback, jobject jpostProcess, + jint desiredWidth, jint desiredHeight, jobject jsubset, + jboolean requireMutable, jint allocator, + jboolean requireUnpremul, jboolean preferRamOverQuality, + jboolean asAlphaMask) { + auto* decoder = reinterpret_cast<ImageDecoder*>(nativePtr); + SkAndroidCodec* codec = decoder->mCodec.get(); + SkImageInfo decodeInfo = codec->getInfo(); + bool scale = false; + int sampleSize = 1; + if (desiredWidth != decodeInfo.width() || desiredHeight != decodeInfo.height()) { + bool match = false; + if (desiredWidth < decodeInfo.width() && desiredHeight < decodeInfo.height()) { + if (supports_any_down_scale(codec)) { + match = true; + decodeInfo = decodeInfo.makeWH(desiredWidth, desiredHeight); + } else { + int sampleX = decodeInfo.width() / desiredWidth; + int sampleY = decodeInfo.height() / desiredHeight; + sampleSize = std::min(sampleX, sampleY); + SkISize sampledSize = codec->getSampledDimensions(sampleSize); + decodeInfo = decodeInfo.makeWH(sampledSize.width(), sampledSize.height()); + if (decodeInfo.width() == desiredWidth && decodeInfo.height() == desiredHeight) { + match = true; + } + } + } + if (!match) { + scale = true; + if (requireUnpremul && kOpaque_SkAlphaType != decodeInfo.alphaType()) { + doThrowISE(env, "Cannot scale unpremultiplied pixels!"); + return nullptr; + } + } + } + + switch (decodeInfo.alphaType()) { + case kUnpremul_SkAlphaType: + if (!requireUnpremul) { + decodeInfo = decodeInfo.makeAlphaType(kPremul_SkAlphaType); + } + break; + case kPremul_SkAlphaType: + if (requireUnpremul) { + decodeInfo = decodeInfo.makeAlphaType(kUnpremul_SkAlphaType); + } + break; + case kOpaque_SkAlphaType: + break; + case kUnknown_SkAlphaType: + return nullObjectReturn("Unknown alpha type"); + } + + SkColorType colorType = kN32_SkColorType; + if (asAlphaMask && decodeInfo.colorType() == kGray_8_SkColorType) { + // We have to trick Skia to decode this to a single channel. + colorType = kGray_8_SkColorType; + } else if (preferRamOverQuality) { + // FIXME: The post-process might add alpha, which would make a 565 + // result incorrect. If we call the postProcess before now and record + // to a picture, we can know whether alpha was added, and if not, we + // can still use 565. + if (decodeInfo.alphaType() == kOpaque_SkAlphaType && !jpostProcess) { + // If the final result will be hardware, decoding to 565 and then + // uploading to the gpu as 8888 will not save memory. This still + // may save us from using F16, but do not go down to 565. + if (allocator != ImageDecoder::kHardware_Allocator && + (allocator != ImageDecoder::kDefault_Allocator || requireMutable)) { + colorType = kRGB_565_SkColorType; + } + } + // Otherwise, stick with N32 + } else { + // This is currently the only way to know that we should decode to F16. + colorType = codec->computeOutputColorType(colorType); + } + sk_sp<SkColorSpace> colorSpace = codec->computeOutputColorSpace(colorType); + decodeInfo = decodeInfo.makeColorType(colorType).makeColorSpace(colorSpace); + + SkBitmap bm; + auto bitmapInfo = decodeInfo; + if (asAlphaMask && colorType == kGray_8_SkColorType) { + bitmapInfo = bitmapInfo.makeColorType(kAlpha_8_SkColorType); + } + if (!bm.setInfo(bitmapInfo)) { + return nullObjectReturn("Failed to setInfo properly"); + } + + sk_sp<Bitmap> nativeBitmap; + // If we are going to scale or subset, we will create a new bitmap later on, + // so use the heap for the temporary. + // FIXME: Use scanline decoding on only a couple lines to save memory. b/70709380. + if (allocator == ImageDecoder::kSharedMemory_Allocator && !scale && !jsubset) { + nativeBitmap = Bitmap::allocateAshmemBitmap(&bm); + } else { + nativeBitmap = Bitmap::allocateHeapBitmap(&bm); + } + if (!nativeBitmap) { + ALOGE("OOM allocating Bitmap with dimensions %i x %i", + decodeInfo.width(), decodeInfo.height()); + doThrowOOME(env); + return nullptr; + } + + jobject jexception = nullptr; + SkAndroidCodec::AndroidOptions options; + options.fSampleSize = sampleSize; + auto result = codec->getAndroidPixels(decodeInfo, bm.getPixels(), bm.rowBytes(), &options); + switch (result) { + case SkCodec::kSuccess: + break; + case SkCodec::kIncompleteInput: + if (jcallback) { + jexception = env->NewObject(gIncomplete_class, gIncomplete_constructorMethodID); + } + break; + case SkCodec::kErrorInInput: + if (jcallback) { + jexception = env->NewObject(gCorrupt_class, gCorrupt_constructorMethodID); + } + break; + default: + ALOGE("getPixels failed with error %i", result); + return nullptr; + } + + if (jexception) { + if (!env->CallBooleanMethod(jcallback, gCallback_onExceptionMethodID, jexception) || + env->ExceptionCheck()) { + return nullptr; + } + } + + float scaleX = 1.0f; + float scaleY = 1.0f; + if (scale) { + scaleX = (float) desiredWidth / decodeInfo.width(); + scaleY = (float) desiredHeight / decodeInfo.height(); + } + + jbyteArray ninePatchChunk = nullptr; + jobject ninePatchInsets = nullptr; + + // Ignore ninepatch when post-processing. + if (!jpostProcess) { + // FIXME: Share more code with BitmapFactory.cpp. + if (decoder->mPeeker.mPatch != nullptr) { + if (scale) { + decoder->mPeeker.scale(scaleX, scaleY, desiredWidth, desiredHeight); + } + size_t ninePatchArraySize = decoder->mPeeker.mPatch->serializedSize(); + ninePatchChunk = env->NewByteArray(ninePatchArraySize); + if (ninePatchChunk == nullptr) { + return nullObjectReturn("ninePatchChunk == null"); + } + + env->SetByteArrayRegion(ninePatchChunk, 0, decoder->mPeeker.mPatchSize, + reinterpret_cast<jbyte*>(decoder->mPeeker.mPatch)); + } + + if (decoder->mPeeker.mHasInsets) { + ninePatchInsets = decoder->mPeeker.createNinePatchInsets(env, 1.0f); + if (ninePatchInsets == nullptr) { + return nullObjectReturn("nine patch insets == null"); + } + } + } + + if (scale || jsubset) { + int translateX = 0; + int translateY = 0; + if (jsubset) { + SkIRect subset; + GraphicsJNI::jrect_to_irect(env, jsubset, &subset); + + // FIXME: If there is no scale, should this instead call + // SkBitmap::extractSubset? If we could upload a subset + // (b/70626068), this would save memory and time. Even for a + // software Bitmap, the extra speed might be worth the memory + // tradeoff if the subset is large? + translateX = -subset.fLeft; + translateY = -subset.fTop; + desiredWidth = subset.width(); + desiredHeight = subset.height(); + } + SkImageInfo scaledInfo = bitmapInfo.makeWH(desiredWidth, desiredHeight); + SkBitmap scaledBm; + if (!scaledBm.setInfo(scaledInfo)) { + nullObjectReturn("Failed scaled setInfo"); + } + + sk_sp<Bitmap> scaledPixelRef; + if (allocator == ImageDecoder::kSharedMemory_Allocator) { + scaledPixelRef = Bitmap::allocateAshmemBitmap(&scaledBm); + } else { + scaledPixelRef = Bitmap::allocateHeapBitmap(&scaledBm); + } + if (!scaledPixelRef) { + ALOGE("OOM allocating scaled Bitmap with dimensions %i x %i", + desiredWidth, desiredHeight); + doThrowOOME(env); + return nullptr; + } + + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); + paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering + + SkCanvas canvas(scaledBm, SkCanvas::ColorBehavior::kLegacy); + canvas.translate(translateX, translateY); + canvas.scale(scaleX, scaleY); + canvas.drawBitmap(bm, 0.0f, 0.0f, &paint); + + bm.swap(scaledBm); + nativeBitmap = scaledPixelRef; + } + + if (jpostProcess) { + std::unique_ptr<Canvas> canvas(Canvas::create_canvas(bm)); + if (!canvas) { + return nullObjectReturn("Failed to create Canvas for PostProcess!"); + } + jobject jcanvas = env->NewObject(gCanvas_class, gCanvas_constructorMethodID, + reinterpret_cast<jlong>(canvas.get())); + if (!jcanvas) { + return nullObjectReturn("Failed to create Java Canvas for PostProcess!"); + } + // jcanvas will now own canvas. + canvas.release(); + + jint pixelFormat = env->CallIntMethod(jpostProcess, gPostProcess_postProcessMethodID, + jcanvas, bm.width(), bm.height()); + if (env->ExceptionCheck()) { + return nullptr; + } + + // The Canvas objects are no longer needed, and will not remain valid. + env->CallVoidMethod(jcanvas, gCanvas_releaseMethodID); + if (env->ExceptionCheck()) { + return nullptr; + } + + SkAlphaType newAlphaType = bm.alphaType(); + switch (pixelFormat) { + case ImageDecoder::kUnknown: + break; + case ImageDecoder::kTranslucent: + newAlphaType = kPremul_SkAlphaType; + break; + case ImageDecoder::kOpaque: + newAlphaType = kOpaque_SkAlphaType; + break; + default: + ALOGE("invalid return from postProcess: %i", pixelFormat); + doThrowIAE(env); + return nullptr; + } + + if (newAlphaType != bm.alphaType()) { + if (!bm.setAlphaType(newAlphaType)) { + ALOGE("incompatible return from postProcess: %i", pixelFormat); + doThrowIAE(env); + return nullptr; + } + nativeBitmap->setAlphaType(newAlphaType); + } + } + + int bitmapCreateFlags = 0x0; + if (!requireUnpremul) { + // Even if the image is opaque, setting this flag means that + // if alpha is added (e.g. by PostProcess), it will be marked as + // premultiplied. + bitmapCreateFlags |= bitmap::kBitmapCreateFlag_Premultiplied; + } + + if (requireMutable) { + bitmapCreateFlags |= bitmap::kBitmapCreateFlag_Mutable; + } else { + if ((allocator == ImageDecoder::kDefault_Allocator || + allocator == ImageDecoder::kHardware_Allocator) + && bm.colorType() != kAlpha_8_SkColorType) + { + sk_sp<Bitmap> hwBitmap = Bitmap::allocateHardwareBitmap(bm); + if (hwBitmap) { + hwBitmap->setImmutable(); + return bitmap::createBitmap(env, hwBitmap.release(), bitmapCreateFlags, + ninePatchChunk, ninePatchInsets); + } + if (allocator == ImageDecoder::kHardware_Allocator) { + return nullObjectReturn("failed to allocate hardware Bitmap!"); + } + // If we failed to create a hardware bitmap, go ahead and create a + // software one. + } + + nativeBitmap->setImmutable(); + } + return bitmap::createBitmap(env, nativeBitmap.release(), bitmapCreateFlags, ninePatchChunk, + ninePatchInsets); +} + +static jobject ImageDecoder_nGetSampledSize(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, + jint sampleSize) { + auto* decoder = reinterpret_cast<ImageDecoder*>(nativePtr); + SkISize size = decoder->mCodec->getSampledDimensions(sampleSize); + return env->NewObject(gPoint_class, gPoint_constructorMethodID, size.width(), size.height()); +} + +static void ImageDecoder_nGetPadding(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, + jobject outPadding) { + auto* decoder = reinterpret_cast<ImageDecoder*>(nativePtr); + decoder->mPeeker.getPadding(env, outPadding); +} + +static void ImageDecoder_nRecycle(JNIEnv* /*env*/, jobject /*clazz*/, jlong nativePtr) { + delete reinterpret_cast<ImageDecoder*>(nativePtr); +} + +static const JNINativeMethod gImageDecoderMethods[] = { + { "nCreate", "(J)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreate }, + { "nCreate", "(Ljava/nio/ByteBuffer;II)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreateByteBuffer }, + { "nCreate", "([BII)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreateByteArray }, + { "nDecodeBitmap", "(JLandroid/graphics/ImageDecoder$OnExceptionListener;Landroid/graphics/PostProcess;IILandroid/graphics/Rect;ZIZZZ)Landroid/graphics/Bitmap;", + (void*) ImageDecoder_nDecodeBitmap }, + { "nGetSampledSize","(JI)Landroid/graphics/Point;", (void*) ImageDecoder_nGetSampledSize }, + { "nGetPadding", "(JLandroid/graphics/Rect;)V", (void*) ImageDecoder_nGetPadding }, + { "nRecycle", "(J)V", (void*) ImageDecoder_nRecycle}, +}; + +int register_android_graphics_ImageDecoder(JNIEnv* env) { + gImageDecoder_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ImageDecoder")); + gImageDecoder_constructorMethodID = GetMethodIDOrDie(env, gImageDecoder_class, "<init>", "(JII)V"); + + gPoint_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Point")); + gPoint_constructorMethodID = GetMethodIDOrDie(env, gPoint_class, "<init>", "(II)V"); + + gIncomplete_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ImageDecoder$IncompleteException")); + gIncomplete_constructorMethodID = GetMethodIDOrDie(env, gIncomplete_class, "<init>", "()V"); + + gCorrupt_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ImageDecoder$CorruptException")); + gCorrupt_constructorMethodID = GetMethodIDOrDie(env, gCorrupt_class, "<init>", "()V"); + + jclass callback_class = FindClassOrDie(env, "android/graphics/ImageDecoder$OnExceptionListener"); + gCallback_onExceptionMethodID = GetMethodIDOrDie(env, callback_class, "onException", "(Ljava/lang/Exception;)Z"); + + jclass postProcess_class = FindClassOrDie(env, "android/graphics/PostProcess"); + gPostProcess_postProcessMethodID = GetMethodIDOrDie(env, postProcess_class, "postProcess", "(Landroid/graphics/Canvas;II)I"); + + gCanvas_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Canvas")); + gCanvas_constructorMethodID = GetMethodIDOrDie(env, gCanvas_class, "<init>", "(J)V"); + gCanvas_releaseMethodID = GetMethodIDOrDie(env, gCanvas_class, "release", "()V"); + + return android::RegisterMethodsOrDie(env, "android/graphics/ImageDecoder", gImageDecoderMethods, + NELEM(gImageDecoderMethods)); +} diff --git a/core/jni/android/graphics/NinePatch.cpp b/core/jni/android/graphics/NinePatch.cpp index 564afeb39490..261910727db9 100644 --- a/core/jni/android/graphics/NinePatch.cpp +++ b/core/jni/android/graphics/NinePatch.cpp @@ -29,11 +29,15 @@ #include "SkLatticeIter.h" #include "SkRegion.h" #include "GraphicsJNI.h" +#include "NinePatchPeeker.h" #include "NinePatchUtils.h" #include <nativehelper/JNIHelp.h> #include "core_jni_helpers.h" +jclass gInsetStruct_class; +jmethodID gInsetStruct_constructorMethodID; + using namespace android; /** @@ -128,6 +132,30 @@ public: }; +jobject NinePatchPeeker::createNinePatchInsets(JNIEnv* env, float scale) const { + if (!mHasInsets) { + return nullptr; + } + + return env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID, + mOpticalInsets[0], mOpticalInsets[1], + mOpticalInsets[2], mOpticalInsets[3], + mOutlineInsets[0], mOutlineInsets[1], + mOutlineInsets[2], mOutlineInsets[3], + mOutlineRadius, mOutlineAlpha, scale); +} + +void NinePatchPeeker::getPadding(JNIEnv* env, jobject outPadding) const { + if (mPatch) { + GraphicsJNI::set_jrect(env, outPadding, + mPatch->paddingLeft, mPatch->paddingTop, + mPatch->paddingRight, mPatch->paddingBottom); + + } else { + GraphicsJNI::set_jrect(env, outPadding, -1, -1, -1, -1); + } +} + ///////////////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gNinePatchMethods[] = { @@ -140,6 +168,10 @@ static const JNINativeMethod gNinePatchMethods[] = { }; int register_android_graphics_NinePatch(JNIEnv* env) { + gInsetStruct_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, + "android/graphics/NinePatch$InsetStruct")); + gInsetStruct_constructorMethodID = GetMethodIDOrDie(env, gInsetStruct_class, "<init>", + "(IIIIIIIIFIF)V"); return android::RegisterMethodsOrDie(env, "android/graphics/NinePatch", gNinePatchMethods, NELEM(gNinePatchMethods)); } diff --git a/core/jni/android/graphics/NinePatchPeeker.cpp b/core/jni/android/graphics/NinePatchPeeker.cpp index 1ea5650bc20b..066d47b4c108 100644 --- a/core/jni/android/graphics/NinePatchPeeker.cpp +++ b/core/jni/android/graphics/NinePatchPeeker.cpp @@ -16,7 +16,8 @@ #include "NinePatchPeeker.h" -#include "SkBitmap.h" +#include <SkBitmap.h> +#include <cutils/compiler.h> using namespace android; @@ -46,3 +47,42 @@ bool NinePatchPeeker::readChunk(const char tag[], const void* data, size_t lengt } return true; // keep on decoding } + +static void scaleDivRange(int32_t* divs, int count, float scale, int maxValue) { + for (int i = 0; i < count; i++) { + divs[i] = int32_t(divs[i] * scale + 0.5f); + if (i > 0 && divs[i] == divs[i - 1]) { + divs[i]++; // avoid collisions + } + } + + if (CC_UNLIKELY(divs[count - 1] > maxValue)) { + // if the collision avoidance above put some divs outside the bounds of the bitmap, + // slide outer stretchable divs inward to stay within bounds + int highestAvailable = maxValue; + for (int i = count - 1; i >= 0; i--) { + divs[i] = highestAvailable; + if (i > 0 && divs[i] <= divs[i-1]) { + // keep shifting + highestAvailable = divs[i] - 1; + } else { + break; + } + } + } +} + +void NinePatchPeeker::scale(float scaleX, float scaleY, int scaledWidth, int scaledHeight) { + if (!mPatch) { + return; + } + mPatch->paddingLeft = int(mPatch->paddingLeft * scaleX + 0.5f); + mPatch->paddingTop = int(mPatch->paddingTop * scaleY + 0.5f); + mPatch->paddingRight = int(mPatch->paddingRight * scaleX + 0.5f); + mPatch->paddingBottom = int(mPatch->paddingBottom * scaleY + 0.5f); + + // The max value for the divRange is one pixel less than the actual max to ensure that the size + // of the last div is not zero. A div of size 0 is considered invalid input and will not render. + scaleDivRange(mPatch->getXDivs(), mPatch->numXDivs, scaleX, scaledWidth - 1); + scaleDivRange(mPatch->getYDivs(), mPatch->numYDivs, scaleY, scaledHeight - 1); +} diff --git a/core/jni/android/graphics/NinePatchPeeker.h b/core/jni/android/graphics/NinePatchPeeker.h index 126eab25fc30..e4e58dda4783 100644 --- a/core/jni/android/graphics/NinePatchPeeker.h +++ b/core/jni/android/graphics/NinePatchPeeker.h @@ -20,7 +20,7 @@ #include "SkPngChunkReader.h" #include <androidfw/ResourceTypes.h> -class SkImageDecoder; +#include <jni.h> using namespace android; @@ -42,9 +42,14 @@ public: bool readChunk(const char tag[], const void* data, size_t length) override; + jobject createNinePatchInsets(JNIEnv*, float scale) const; + void getPadding(JNIEnv*, jobject outPadding) const; + void scale(float scaleX, float scaleY, int scaledWidth, int scaledHeight); + Res_png_9patch* mPatch; size_t mPatchSize; bool mHasInsets; +private: int32_t mOpticalInsets[4]; int32_t mOutlineInsets[4]; float mOutlineRadius; diff --git a/core/jni/android/opengl/util.cpp b/core/jni/android/opengl/util.cpp index 1522c20dbe2f..1676d4b91e3d 100644 --- a/core/jni/android/opengl/util.cpp +++ b/core/jni/android/opengl/util.cpp @@ -25,7 +25,8 @@ #include <assert.h> #include <dlfcn.h> -#include <GLES/gl.h> +#include <GLES2/gl2.h> +#include <GLES2/gl2ext.h> #include <ETC1/etc1.h> #include <SkBitmap.h> @@ -639,6 +640,10 @@ static int checkFormat(SkColorType colorType, int format, int type) return 0; } break; + case kRGBA_F16_SkColorType: + if (type == GL_HALF_FLOAT_OES && format == PIXEL_FORMAT_RGBA_FP16) + return 0; + break; default: break; } @@ -656,6 +661,8 @@ static int getInternalFormat(SkColorType colorType) return GL_RGBA; case kRGB_565_SkColorType: return GL_RGB; + case kRGBA_F16_SkColorType: + return PIXEL_FORMAT_RGBA_FP16; default: return -1; } @@ -672,6 +679,8 @@ static int getType(SkColorType colorType) return GL_UNSIGNED_BYTE; case kRGB_565_SkColorType: return GL_UNSIGNED_SHORT_5_6_5; + case kRGBA_F16_SkColorType: + return GL_HALF_FLOAT_OES; default: return -1; } diff --git a/core/res/res/drawable/ic_wifi_settings.xml b/core/res/res/drawable/ic_wifi_settings.xml new file mode 100644 index 000000000000..c678ad4ddc47 --- /dev/null +++ b/core/res/res/drawable/ic_wifi_settings.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:pathData="M 0 0 H 24 V 24 H 0 V 0 Z" /> + <path + android:fillColor="#000000" + android:pathData="M12.584,15.93c0.026-0.194,0.044-0.397,0.044-0.608c0-0.211-0.018-0.405-0.044-0.608l1.304-1.022 +c0.115-0.088,0.15-0.256,0.071-0.397l-1.234-2.133c-0.071-0.132-0.238-0.185-0.379-0.132l-1.533,0.617 +c-0.317-0.247-0.67-0.449-1.04-0.608L9.535,9.4c-0.018-0.132-0.141-0.247-0.3-0.247H6.768c-0.15,0-0.282,0.115-0.3,0.256 +L6.23,11.048c-0.379,0.159-0.723,0.361-1.04,0.608l-1.533-0.617c-0.141-0.053-0.3,0-0.379,0.132l-1.234,2.133 +c-0.079,0.132-0.044,0.3,0.07,0.397l1.304,1.022c-0.026,0.194-0.044,0.405-0.044,0.608s0.018,0.405,0.044,0.608l-1.304,1.022 +c-0.115,0.088-0.15,0.256-0.07,0.397l1.234,2.133c0.07,0.132,0.238,0.185,0.379,0.132l1.533-0.617 +c0.317,0.247,0.67,0.449,1.04,0.608l0.238,1.639c0.018,0.15,0.15,0.256,0.3,0.256h2.467c0.159,0,0.282-0.115,0.3-0.256 +l0.238-1.639c0.379-0.15,0.723-0.361,1.04-0.608l1.533,0.617c0.141,0.053,0.3,0,0.379-0.132l1.234-2.133 +c0.071-0.132,0.044-0.3-0.07-0.397L12.584,15.93z +M8.002,17.481c-1.19,0-2.159-0.969-2.159-2.159s0.969-2.159,2.159-2.159 +s2.159,0.969,2.159,2.159C10.161,16.512,9.191,17.481,8.002,17.481z" /> + <path + android:fillColor="#000000" + android:pathData="M16.003,12.026l5.995-7.474c-0.229-0.172-2.537-2.06-6-2.06s-5.771,1.889-6,2.06l5.995,7.469l0.005,0.01L16.003,12.026z" /> +</vector>
\ No newline at end of file diff --git a/core/res/res/layout/notification_template_material_ambient.xml b/core/res/res/layout/notification_template_material_ambient.xml index 865685ff26f5..435289de92de 100644 --- a/core/res/res/layout/notification_template_material_ambient.xml +++ b/core/res/res/layout/notification_template_material_ambient.xml @@ -72,7 +72,7 @@ android:textColor="#eeffffff" android:layout_marginTop="4dp" android:ellipsize="end" - android:maxLines="7" + android:maxLines="3" /> </LinearLayout> </LinearLayout> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 4b2424ffd213..d962adaff974 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1308,6 +1308,7 @@ <java-symbol type="drawable" name="platlogo" /> <java-symbol type="drawable" name="stat_notify_sync_error" /> <java-symbol type="drawable" name="stat_notify_wifi_in_range" /> + <java-symbol type="drawable" name="ic_wifi_settings" /> <java-symbol type="drawable" name="ic_wifi_signal_0" /> <java-symbol type="drawable" name="ic_wifi_signal_1" /> <java-symbol type="drawable" name="ic_wifi_signal_2" /> diff --git a/core/tests/coretests/src/android/animation/AnimatorInflaterTest.java b/core/tests/coretests/src/android/animation/AnimatorInflaterTest.java index 3c8185328582..e26bdf53b872 100644 --- a/core/tests/coretests/src/android/animation/AnimatorInflaterTest.java +++ b/core/tests/coretests/src/android/animation/AnimatorInflaterTest.java @@ -15,6 +15,7 @@ */ package android.animation; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase2; import java.util.HashSet; @@ -24,6 +25,7 @@ import java.util.concurrent.TimeUnit; import com.android.frameworks.coretests.R; +@LargeTest public class AnimatorInflaterTest extends ActivityInstrumentationTestCase2<BasicAnimatorActivity> { Set<Integer> identityHashes = new HashSet<Integer>(); diff --git a/core/tests/coretests/src/android/animation/StateListAnimatorTest.java b/core/tests/coretests/src/android/animation/StateListAnimatorTest.java index 38df78d3bad5..a9961e19e0c6 100644 --- a/core/tests/coretests/src/android/animation/StateListAnimatorTest.java +++ b/core/tests/coretests/src/android/animation/StateListAnimatorTest.java @@ -17,6 +17,7 @@ package android.animation; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase2; import android.test.UiThreadTest; import android.util.StateSet; @@ -27,7 +28,7 @@ import com.android.frameworks.coretests.R; import java.util.concurrent.atomic.AtomicInteger; - +@LargeTest public class StateListAnimatorTest extends ActivityInstrumentationTestCase2<BasicAnimatorActivity> { public StateListAnimatorTest() { diff --git a/core/tests/coretests/src/android/app/ApplicationPackageManagerTest.java b/core/tests/coretests/src/android/app/ApplicationPackageManagerTest.java index f60bf94f8c99..063bef7387c6 100644 --- a/core/tests/coretests/src/android/app/ApplicationPackageManagerTest.java +++ b/core/tests/coretests/src/android/app/ApplicationPackageManagerTest.java @@ -22,6 +22,7 @@ import android.content.pm.IPackageManager; import android.content.pm.PackageInfo; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; +import android.support.test.filters.LargeTest; import junit.framework.TestCase; @@ -34,6 +35,7 @@ import java.util.List; import static android.os.storage.VolumeInfo.STATE_MOUNTED; import static android.os.storage.VolumeInfo.STATE_UNMOUNTED; +@LargeTest public class ApplicationPackageManagerTest extends TestCase { private static final String sInternalVolPath = "/data"; private static final String sAdoptedVolPath = "/mnt/expand/123"; diff --git a/core/tests/coretests/src/android/app/InstrumentationTest.java b/core/tests/coretests/src/android/app/InstrumentationTest.java index ee3834cabbc2..9b59da48e760 100644 --- a/core/tests/coretests/src/android/app/InstrumentationTest.java +++ b/core/tests/coretests/src/android/app/InstrumentationTest.java @@ -17,8 +17,10 @@ package android.app; import android.os.Bundle; +import android.support.test.filters.LargeTest; import android.test.InstrumentationTestCase; +@LargeTest public class InstrumentationTest extends InstrumentationTestCase { /** diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index c14dc90dbacd..718393410d3b 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -214,6 +214,20 @@ public class NotificationTest { assertTrue(n.allPendingIntents.contains(intent)); } + @Test + public void testMessagingStyle_isGroupConversation() { + Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle("self name") + .setGroupConversation(true); + Notification notification = new Notification.Builder(mContext, "test id") + .setSmallIcon(1) + .setContentTitle("test title") + .setStyle(messagingStyle) + .build(); + + assertTrue(messagingStyle.isGroupConversation()); + assertTrue(notification.extras.getBoolean(Notification.EXTRA_IS_GROUP_CONVERSATION)); + } + private Notification.Builder getMediaNotification() { MediaSession session = new MediaSession(mContext, "test"); return new Notification.Builder(mContext, "color") diff --git a/core/tests/coretests/src/android/app/activity/BroadcastTest.java b/core/tests/coretests/src/android/app/activity/BroadcastTest.java index e9e8bfcb09c5..13e70ebdb2e3 100644 --- a/core/tests/coretests/src/android/app/activity/BroadcastTest.java +++ b/core/tests/coretests/src/android/app/activity/BroadcastTest.java @@ -27,12 +27,13 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.UserHandle; +import android.support.test.filters.LargeTest; import android.test.FlakyTest; -import android.test.suitebuilder.annotation.Suppress; import android.util.Log; import java.util.Arrays; +@LargeTest public class BroadcastTest extends ActivityTestsBase { public static final int BROADCAST_TIMEOUT = 5 * 1000; diff --git a/core/tests/coretests/src/android/app/activity/IntentSenderTest.java b/core/tests/coretests/src/android/app/activity/IntentSenderTest.java index 3c309154eedc..8c1d79b5d89c 100644 --- a/core/tests/coretests/src/android/app/activity/IntentSenderTest.java +++ b/core/tests/coretests/src/android/app/activity/IntentSenderTest.java @@ -20,10 +20,10 @@ import android.app.Activity; import android.app.PendingIntent; import android.content.Intent; import android.content.IntentFilter; -import android.test.suitebuilder.annotation.Suppress; import android.os.Bundle; -import android.test.suitebuilder.annotation.Suppress; +import android.support.test.filters.LargeTest; +@LargeTest public class IntentSenderTest extends BroadcastTest { public void testRegisteredReceivePermissionGranted() throws Exception { diff --git a/core/tests/coretests/src/android/app/backup/BackupDataTest.java b/core/tests/coretests/src/android/app/backup/BackupDataTest.java index 0c204e098405..5b8e481cf573 100644 --- a/core/tests/coretests/src/android/app/backup/BackupDataTest.java +++ b/core/tests/coretests/src/android/app/backup/BackupDataTest.java @@ -23,6 +23,7 @@ import android.content.res.AssetManager; import android.os.Bundle; import android.os.Environment; import android.os.ParcelFileDescriptor; +import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; import android.test.InstrumentationTestCase; import android.util.Base64; @@ -41,6 +42,7 @@ import java.io.InputStreamReader; import java.lang.Exception; import java.nio.ByteBuffer; +@LargeTest public class BackupDataTest extends AndroidTestCase { private static final String KEY1 = "key1"; private static final String KEY2 = "key2a"; diff --git a/core/tests/coretests/src/android/app/backup/FullBackupTest.java b/core/tests/coretests/src/android/app/backup/FullBackupTest.java index 3869cd29f69f..bc6fc15db163 100644 --- a/core/tests/coretests/src/android/app/backup/FullBackupTest.java +++ b/core/tests/coretests/src/android/app/backup/FullBackupTest.java @@ -20,6 +20,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; +import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; import android.util.ArrayMap; import android.util.ArraySet; @@ -36,6 +37,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +@LargeTest public class FullBackupTest extends AndroidTestCase { private XmlPullParserFactory mFactory; private XmlPullParser mXpp; diff --git a/core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java b/core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java index 70a0877beb5f..e20645c7d3c8 100644 --- a/core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java +++ b/core/tests/coretests/src/android/app/timezone/DistroFormatVersionTest.java @@ -21,12 +21,14 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.os.Parcel; +import android.support.test.filters.LargeTest; import org.junit.Test; /** * Tests for {@link DistroFormatVersion}. */ +@LargeTest public class DistroFormatVersionTest { @Test diff --git a/core/tests/coretests/src/android/app/timezone/DistroRulesVersionTest.java b/core/tests/coretests/src/android/app/timezone/DistroRulesVersionTest.java index eecae46910fd..b69054cebbd2 100644 --- a/core/tests/coretests/src/android/app/timezone/DistroRulesVersionTest.java +++ b/core/tests/coretests/src/android/app/timezone/DistroRulesVersionTest.java @@ -21,12 +21,14 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.os.Parcel; +import android.support.test.filters.LargeTest; import org.junit.Test; /** * Tests for {@link DistroRulesVersion}. */ +@LargeTest public class DistroRulesVersionTest { @Test diff --git a/core/tests/coretests/src/android/app/timezone/RulesStateTest.java b/core/tests/coretests/src/android/app/timezone/RulesStateTest.java index 99abe243556c..dd462403ed82 100644 --- a/core/tests/coretests/src/android/app/timezone/RulesStateTest.java +++ b/core/tests/coretests/src/android/app/timezone/RulesStateTest.java @@ -23,12 +23,14 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.os.Parcel; +import android.support.test.filters.LargeTest; import org.junit.Test; /** * Tests for {@link RulesState}. */ +@LargeTest public class RulesStateTest { @Test diff --git a/core/tests/coretests/src/android/app/timezone/RulesUpdaterContractTest.java b/core/tests/coretests/src/android/app/timezone/RulesUpdaterContractTest.java index 91f8ebc9ec1c..e4aac509c2ef 100644 --- a/core/tests/coretests/src/android/app/timezone/RulesUpdaterContractTest.java +++ b/core/tests/coretests/src/android/app/timezone/RulesUpdaterContractTest.java @@ -24,6 +24,7 @@ import static org.mockito.hamcrest.MockitoHamcrest.argThat; import android.content.Context; import android.content.Intent; +import android.support.test.filters.LargeTest; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; @@ -33,6 +34,7 @@ import org.junit.Test; /** * Tests for {@link RulesUpdaterContract}. */ +@LargeTest public class RulesUpdaterContractTest { @Test diff --git a/core/tests/coretests/src/android/content/RestrictionsManagerTest.java b/core/tests/coretests/src/android/content/RestrictionsManagerTest.java index d92eece8afca..10d74f7d2945 100644 --- a/core/tests/coretests/src/android/content/RestrictionsManagerTest.java +++ b/core/tests/coretests/src/android/content/RestrictionsManagerTest.java @@ -17,6 +17,7 @@ package android.content; import android.os.Bundle; import android.os.Parcelable; +import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; import java.util.Arrays; @@ -24,6 +25,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +@LargeTest public class RestrictionsManagerTest extends AndroidTestCase { private RestrictionsManager mRm; diff --git a/core/tests/coretests/src/android/content/pm/MacAuthenticatedInputStreamTest.java b/core/tests/coretests/src/android/content/pm/MacAuthenticatedInputStreamTest.java index 948e7220978b..659f9ea77956 100644 --- a/core/tests/coretests/src/android/content/pm/MacAuthenticatedInputStreamTest.java +++ b/core/tests/coretests/src/android/content/pm/MacAuthenticatedInputStreamTest.java @@ -16,6 +16,7 @@ package android.content.pm; +import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; import java.io.ByteArrayInputStream; @@ -27,6 +28,7 @@ import javax.crypto.spec.SecretKeySpec; import libcore.io.Streams; +@LargeTest public class MacAuthenticatedInputStreamTest extends AndroidTestCase { private static final SecretKey HMAC_KEY_1 = new SecretKeySpec("test_key_1".getBytes(), "HMAC"); diff --git a/core/tests/coretests/src/android/content/pm/ParceledListSliceTest.java b/core/tests/coretests/src/android/content/pm/ParceledListSliceTest.java index a9d19b4b295c..952bb5530c15 100644 --- a/core/tests/coretests/src/android/content/pm/ParceledListSliceTest.java +++ b/core/tests/coretests/src/android/content/pm/ParceledListSliceTest.java @@ -2,6 +2,7 @@ package android.content.pm; import android.os.Parcel; import android.os.Parcelable; +import android.support.test.filters.LargeTest; import junit.framework.TestCase; @@ -9,6 +10,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +@LargeTest public class ParceledListSliceTest extends TestCase { public void testSmallList() throws Exception { diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java index 271c6392bdf4..d3d1f22af3cb 100644 --- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java +++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java @@ -21,6 +21,7 @@ import android.os.FileUtils; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; +import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; import android.util.AttributeSet; import android.util.SparseArray; @@ -43,6 +44,7 @@ import java.util.Set; /** * Tests for {@link android.content.pm.RegisteredServicesCache} */ +@LargeTest public class RegisteredServicesCacheTest extends AndroidTestCase { private static final int U0 = 0; private static final int U1 = 1; diff --git a/core/tests/coretests/src/android/content/pm/SignatureTest.java b/core/tests/coretests/src/android/content/pm/SignatureTest.java index 89d599742e90..a3fa1a9c06cf 100644 --- a/core/tests/coretests/src/android/content/pm/SignatureTest.java +++ b/core/tests/coretests/src/android/content/pm/SignatureTest.java @@ -16,8 +16,11 @@ package android.content.pm; +import android.support.test.filters.LargeTest; + import junit.framework.TestCase; +@LargeTest public class SignatureTest extends TestCase { /** Cert A with valid syntax */ diff --git a/core/tests/coretests/src/android/content/pm/VerificationParamsTest.java b/core/tests/coretests/src/android/content/pm/VerificationParamsTest.java index d963812db622..68942cbd54ad 100644 --- a/core/tests/coretests/src/android/content/pm/VerificationParamsTest.java +++ b/core/tests/coretests/src/android/content/pm/VerificationParamsTest.java @@ -19,6 +19,7 @@ package android.content.pm; import android.content.pm.VerificationParams; import android.net.Uri; import android.os.Parcel; +import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; /** @@ -27,6 +28,7 @@ import android.test.AndroidTestCase; * To test run: * ./development/testrunner/runtest.py frameworks-core -c android.content.pm.VerificationParamsTest */ +@LargeTest public class VerificationParamsTest extends AndroidTestCase { private final static String VERIFICATION_URI_STRING = "http://verification.uri/path"; diff --git a/core/tests/coretests/src/android/content/pm/VerifierDeviceIdentityTest.java b/core/tests/coretests/src/android/content/pm/VerifierDeviceIdentityTest.java index cb13eb7c846e..88d7a59a98e2 100644 --- a/core/tests/coretests/src/android/content/pm/VerifierDeviceIdentityTest.java +++ b/core/tests/coretests/src/android/content/pm/VerifierDeviceIdentityTest.java @@ -17,9 +17,11 @@ package android.content.pm; import android.os.Parcel; +import android.support.test.filters.LargeTest; import java.util.Random; +@LargeTest public class VerifierDeviceIdentityTest extends android.test.AndroidTestCase { private static final long TEST_1 = 0x7A5F00FF5A55AAA5L; diff --git a/core/tests/coretests/src/android/graphics/drawable/AdaptiveIconDrawableTest.java b/core/tests/coretests/src/android/graphics/drawable/AdaptiveIconDrawableTest.java index 7550cb5211b9..b28a4b5ae628 100644 --- a/core/tests/coretests/src/android/graphics/drawable/AdaptiveIconDrawableTest.java +++ b/core/tests/coretests/src/android/graphics/drawable/AdaptiveIconDrawableTest.java @@ -15,6 +15,7 @@ import android.graphics.Path.Direction; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; +import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; import android.util.Log; import android.util.PathParser; @@ -23,6 +24,7 @@ import java.io.FileOutputStream; import java.util.Arrays; import org.junit.Test; +@LargeTest public class AdaptiveIconDrawableTest extends AndroidTestCase { public static final String TAG = AdaptiveIconDrawableTest.class.getSimpleName(); diff --git a/core/tests/coretests/src/android/hardware/display/VirtualDisplayTest.java b/core/tests/coretests/src/android/hardware/display/VirtualDisplayTest.java index a2e9ae89e255..f30b1a29c781 100644 --- a/core/tests/coretests/src/android/hardware/display/VirtualDisplayTest.java +++ b/core/tests/coretests/src/android/hardware/display/VirtualDisplayTest.java @@ -30,6 +30,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; import android.util.DisplayMetrics; import android.util.Log; @@ -49,6 +50,7 @@ import java.util.concurrent.locks.ReentrantLock; * Contains additional tests that cannot be included in CTS because they require * system permissions. See also the CTS version of VirtualDisplayTest. */ +@LargeTest public class VirtualDisplayTest extends AndroidTestCase { private static final String TAG = "VirtualDisplayTest"; diff --git a/core/tests/coretests/src/android/metrics/LogMakerTest.java b/core/tests/coretests/src/android/metrics/LogMakerTest.java index ada59cd84931..3be776deb9f1 100644 --- a/core/tests/coretests/src/android/metrics/LogMakerTest.java +++ b/core/tests/coretests/src/android/metrics/LogMakerTest.java @@ -15,9 +15,13 @@ */ package android.metrics; +import android.support.test.filters.LargeTest; + import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + import junit.framework.TestCase; +@LargeTest public class LogMakerTest extends TestCase { public void testSerialize() { diff --git a/core/tests/coretests/src/android/metrics/MetricsReaderTest.java b/core/tests/coretests/src/android/metrics/MetricsReaderTest.java index d10b3519bccf..784a12fa1f57 100644 --- a/core/tests/coretests/src/android/metrics/MetricsReaderTest.java +++ b/core/tests/coretests/src/android/metrics/MetricsReaderTest.java @@ -16,6 +16,7 @@ package android.metrics; import android.metrics.MetricsReader.Event; +import android.support.test.filters.LargeTest; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -23,6 +24,7 @@ import junit.framework.TestCase; import java.util.Collection; +@LargeTest public class MetricsReaderTest extends TestCase { private static final int FULL_N = 10; private static final int CHECKPOINTED_N = 4; diff --git a/core/tests/coretests/src/android/os/WorkSourceTest.java b/core/tests/coretests/src/android/os/WorkSourceTest.java new file mode 100644 index 000000000000..7350db7a0811 --- /dev/null +++ b/core/tests/coretests/src/android/os/WorkSourceTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.os.WorkSource.WorkChain; + +import junit.framework.TestCase; + +import java.util.List; + +/** + * Provides unit tests for hidden / unstable WorkSource APIs that are not CTS testable. + * + * These tests will be moved to CTS when finalized. + */ +public class WorkSourceTest extends TestCase { + public void testWorkChain_add() { + WorkChain wc1 = new WorkChain(); + wc1.addNode(56, null); + + assertEquals(56, wc1.getUids()[0]); + assertEquals(null, wc1.getTags()[0]); + assertEquals(1, wc1.getSize()); + + wc1.addNode(57, "foo"); + assertEquals(56, wc1.getUids()[0]); + assertEquals(null, wc1.getTags()[0]); + assertEquals(57, wc1.getUids()[1]); + assertEquals("foo", wc1.getTags()[1]); + + assertEquals(2, wc1.getSize()); + } + + public void testWorkChain_equalsHashCode() { + WorkChain wc1 = new WorkChain(); + WorkChain wc2 = new WorkChain(); + + assertEquals(wc1, wc2); + assertEquals(wc1.hashCode(), wc2.hashCode()); + + wc1.addNode(1, null); + wc2.addNode(1, null); + assertEquals(wc1, wc2); + assertEquals(wc1.hashCode(), wc2.hashCode()); + + wc1.addNode(2, "tag"); + wc2.addNode(2, "tag"); + assertEquals(wc1, wc2); + assertEquals(wc1.hashCode(), wc2.hashCode()); + + wc1 = new WorkChain(); + wc2 = new WorkChain(); + wc1.addNode(5, null); + wc2.addNode(6, null); + assertFalse(wc1.equals(wc2)); + assertFalse(wc1.hashCode() == wc2.hashCode()); + + wc1 = new WorkChain(); + wc2 = new WorkChain(); + wc1.addNode(5, "tag1"); + wc2.addNode(5, "tag2"); + assertFalse(wc1.equals(wc2)); + assertFalse(wc1.hashCode() == wc2.hashCode()); + } + + public void testWorkChain_constructor() { + WorkChain wc1 = new WorkChain(); + wc1.addNode(1, "foo") + .addNode(2, null) + .addNode(3, "baz"); + + WorkChain wc2 = new WorkChain(wc1); + assertEquals(wc1, wc2); + + wc1.addNode(4, "baz"); + assertFalse(wc1.equals(wc2)); + } + + public void testDiff_workChains() { + WorkSource ws1 = new WorkSource(); + ws1.add(50); + ws1.createWorkChain().addNode(52, "foo"); + WorkSource ws2 = new WorkSource(); + ws2.add(50); + ws2.createWorkChain().addNode(60, "bar"); + + // Diffs don't take WorkChains into account for the sake of backward compatibility. + assertFalse(ws1.diff(ws2)); + assertFalse(ws2.diff(ws1)); + } + + public void testEquals_workChains() { + WorkSource ws1 = new WorkSource(); + ws1.add(50); + ws1.createWorkChain().addNode(52, "foo"); + + WorkSource ws2 = new WorkSource(); + ws2.add(50); + ws2.createWorkChain().addNode(52, "foo"); + + assertEquals(ws1, ws2); + + // Unequal number of WorkChains. + ws2.createWorkChain().addNode(53, "baz"); + assertFalse(ws1.equals(ws2)); + + // Different WorkChain contents. + WorkSource ws3 = new WorkSource(); + ws3.add(50); + ws3.createWorkChain().addNode(60, "bar"); + + assertFalse(ws1.equals(ws3)); + assertFalse(ws3.equals(ws1)); + } + + public void testWorkSourceParcelling() { + WorkSource ws = new WorkSource(); + + WorkChain wc = ws.createWorkChain(); + wc.addNode(56, "foo"); + wc.addNode(75, "baz"); + WorkChain wc2 = ws.createWorkChain(); + wc2.addNode(20, "foo2"); + wc2.addNode(30, "baz2"); + + Parcel p = Parcel.obtain(); + ws.writeToParcel(p, 0); + p.setDataPosition(0); + + WorkSource unparcelled = WorkSource.CREATOR.createFromParcel(p); + + assertEquals(unparcelled, ws); + } + + public void testSet_workChains() { + WorkSource ws1 = new WorkSource(); + ws1.add(50); + + WorkSource ws2 = new WorkSource(); + ws2.add(60); + WorkChain wc = ws2.createWorkChain(); + wc.addNode(75, "tag"); + + ws1.set(ws2); + + // Assert that the WorkChains are copied across correctly to the new WorkSource object. + List<WorkChain> workChains = ws1.getWorkChains(); + assertEquals(1, workChains.size()); + + assertEquals(1, workChains.get(0).getSize()); + assertEquals(75, workChains.get(0).getUids()[0]); + assertEquals("tag", workChains.get(0).getTags()[0]); + + // Also assert that a deep copy of workchains is made, so the addition of a new WorkChain + // or the modification of an existing WorkChain has no effect. + ws2.createWorkChain(); + assertEquals(1, ws1.getWorkChains().size()); + + wc.addNode(50, "tag2"); + assertEquals(1, ws1.getWorkChains().size()); + assertEquals(1, ws1.getWorkChains().get(0).getSize()); + } + + public void testSet_nullWorkChain() { + WorkSource ws = new WorkSource(); + ws.add(60); + WorkChain wc = ws.createWorkChain(); + wc.addNode(75, "tag"); + + ws.set(null); + assertEquals(0, ws.getWorkChains().size()); + } + + public void testAdd_workChains() { + WorkSource ws = new WorkSource(); + ws.createWorkChain().addNode(70, "foo"); + + WorkSource ws2 = new WorkSource(); + ws2.createWorkChain().addNode(60, "tag"); + + ws.add(ws2); + + // Check that the new WorkChain is added to the end of the list. + List<WorkChain> workChains = ws.getWorkChains(); + assertEquals(2, workChains.size()); + assertEquals(1, workChains.get(1).getSize()); + assertEquals(60, ws.getWorkChains().get(1).getUids()[0]); + assertEquals("tag", ws.getWorkChains().get(1).getTags()[0]); + + // Adding the same WorkChain twice should be a no-op. + ws.add(ws2); + assertEquals(2, workChains.size()); + } +} diff --git a/core/tests/coretests/src/android/preference/ListPreferenceTest.java b/core/tests/coretests/src/android/preference/ListPreferenceTest.java index 41f8e03f46ce..72f62f167989 100644 --- a/core/tests/coretests/src/android/preference/ListPreferenceTest.java +++ b/core/tests/coretests/src/android/preference/ListPreferenceTest.java @@ -17,8 +17,10 @@ package android.preference; import android.preference.ListPreference; +import android.support.test.filters.LargeTest; import android.test.AndroidTestCase; +@LargeTest public class ListPreferenceTest extends AndroidTestCase { public void testListPreferenceSummaryFromEntries() { String[] entries = { "one", "two", "three" }; diff --git a/core/tests/coretests/src/android/text/StaticLayoutTest.java b/core/tests/coretests/src/android/text/StaticLayoutTest.java index f4514b56ec8d..d817278330d4 100644 --- a/core/tests/coretests/src/android/text/StaticLayoutTest.java +++ b/core/tests/coretests/src/android/text/StaticLayoutTest.java @@ -697,9 +697,13 @@ public class StaticLayoutTest { public void testGetOffset_UNICODE_Hebrew() { String testString = "\u05DE\u05E1\u05E2\u05D3\u05D4"; // Hebrew Characters for (CharSequence seq: buildTestCharSequences(testString, Normalizer.Form.values())) { - StaticLayout layout = new StaticLayout(seq, mDefaultPaint, - DEFAULT_OUTER_WIDTH, DEFAULT_ALIGN, - TextDirectionHeuristics.RTL, SPACE_MULTI, SPACE_ADD, true); + StaticLayout.Builder b = StaticLayout.Builder.obtain( + seq, 0, seq.length(), mDefaultPaint, DEFAULT_OUTER_WIDTH) + .setAlignment(DEFAULT_ALIGN) + .setTextDirection(TextDirectionHeuristics.RTL) + .setLineSpacing(SPACE_ADD, SPACE_MULTI) + .setIncludePad(true); + StaticLayout layout = b.build(); String testLabel = buildTestMessage(seq); diff --git a/core/tests/coretests/src/android/transition/TransitionTest.java b/core/tests/coretests/src/android/transition/TransitionTest.java index ab4320c65eb2..7e629f944684 100644 --- a/core/tests/coretests/src/android/transition/TransitionTest.java +++ b/core/tests/coretests/src/android/transition/TransitionTest.java @@ -19,6 +19,7 @@ package android.transition; import android.animation.AnimatorSetActivity; import android.app.Activity; import android.graphics.Rect; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase2; import android.transition.Transition.EpicenterCallback; import android.util.ArrayMap; @@ -30,6 +31,7 @@ import com.android.frameworks.coretests.R; import java.lang.reflect.Field; +@LargeTest public class TransitionTest extends ActivityInstrumentationTestCase2<AnimatorSetActivity> { Activity mActivity; public TransitionTest() { diff --git a/core/tests/coretests/src/android/util/ArrayMapTest.java b/core/tests/coretests/src/android/util/ArrayMapTest.java index 32aa29fa3339..f0cc7f77fe63 100644 --- a/core/tests/coretests/src/android/util/ArrayMapTest.java +++ b/core/tests/coretests/src/android/util/ArrayMapTest.java @@ -14,6 +14,7 @@ package android.util; +import android.support.test.filters.LargeTest; import android.util.ArrayMap; import junit.framework.TestCase; @@ -24,6 +25,7 @@ import java.util.ConcurrentModificationException; /** * Unit tests for ArrayMap that don't belong in CTS. */ +@LargeTest public class ArrayMapTest extends TestCase { private static final String TAG = "ArrayMapTest"; ArrayMap<String, String> map = new ArrayMap<>(); diff --git a/core/tests/coretests/src/android/util/Base64Test.java b/core/tests/coretests/src/android/util/Base64Test.java index 53368d40104e..3aee5839983e 100644 --- a/core/tests/coretests/src/android/util/Base64Test.java +++ b/core/tests/coretests/src/android/util/Base64Test.java @@ -16,15 +16,17 @@ package android.util; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import junit.framework.TestCase; +import android.support.test.filters.LargeTest; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; import java.util.Random; +import junit.framework.TestCase; +@LargeTest public class Base64Test extends TestCase { private static final String TAG = "Base64Test"; diff --git a/core/tests/coretests/src/android/util/LocalLogTest.java b/core/tests/coretests/src/android/util/LocalLogTest.java index a63c8a084964..5144c85e72a2 100644 --- a/core/tests/coretests/src/android/util/LocalLogTest.java +++ b/core/tests/coretests/src/android/util/LocalLogTest.java @@ -16,6 +16,8 @@ package android.util; +import android.support.test.filters.LargeTest; + import junit.framework.TestCase; import java.io.PrintWriter; @@ -24,7 +26,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; - +@LargeTest public class LocalLogTest extends TestCase { public void testA() { diff --git a/core/tests/coretests/src/android/util/LongSparseLongArrayTest.java b/core/tests/coretests/src/android/util/LongSparseLongArrayTest.java index cb468bc68eeb..3f03db9dc1fb 100644 --- a/core/tests/coretests/src/android/util/LongSparseLongArrayTest.java +++ b/core/tests/coretests/src/android/util/LongSparseLongArrayTest.java @@ -16,6 +16,8 @@ package android.util; +import android.support.test.filters.LargeTest; + import junit.framework.TestCase; import java.util.HashMap; @@ -26,6 +28,7 @@ import java.util.Random; /** * Tests for {@link LongSparseLongArray}. */ +@LargeTest public class LongSparseLongArrayTest extends TestCase { private static final String TAG = "LongSparseLongArrayTest"; diff --git a/core/tests/coretests/src/android/view/DisplayCutoutTest.java b/core/tests/coretests/src/android/view/DisplayCutoutTest.java index 6dd787d2e038..0d8c679a312f 100644 --- a/core/tests/coretests/src/android/view/DisplayCutoutTest.java +++ b/core/tests/coretests/src/android/view/DisplayCutoutTest.java @@ -25,6 +25,7 @@ import static org.junit.Assert.assertTrue; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Region; import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.support.test.filters.SmallTest; @@ -34,7 +35,6 @@ import android.view.DisplayCutout.ParcelableWrapper; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.ArrayList; import java.util.Arrays; @RunWith(AndroidJUnit4.class) @@ -45,19 +45,14 @@ public class DisplayCutoutTest { /** This is not a consistent cutout. Useful for verifying insets in one go though. */ final DisplayCutout mCutoutNumbers = new DisplayCutout( new Rect(1, 2, 3, 4), - new Rect(5, 6, 7, 8), - Arrays.asList( - new Point(9, 10), - new Point(11, 12), - new Point(13, 14), - new Point(15, 16))); + new Region(5, 6, 7, 8)); final DisplayCutout mCutoutTop = createCutoutTop(); @Test public void hasCutout() throws Exception { - assertFalse(NO_CUTOUT.hasCutout()); - assertTrue(mCutoutTop.hasCutout()); + assertTrue(NO_CUTOUT.isEmpty()); + assertFalse(mCutoutTop.isEmpty()); } @Test @@ -67,30 +62,12 @@ public class DisplayCutoutTest { assertEquals(3, mCutoutNumbers.getSafeInsetRight()); assertEquals(4, mCutoutNumbers.getSafeInsetBottom()); - Rect safeInsets = new Rect(); - mCutoutNumbers.getSafeInsets(safeInsets); - - assertEquals(new Rect(1, 2, 3, 4), safeInsets); + assertEquals(new Rect(1, 2, 3, 4), mCutoutNumbers.getSafeInsets()); } @Test public void getBoundingRect() throws Exception { - Rect boundingRect = new Rect(); - mCutoutTop.getBoundingRect(boundingRect); - - assertEquals(new Rect(50, 0, 75, 100), boundingRect); - } - - @Test - public void getBoundingPolygon() throws Exception { - ArrayList<Point> boundingPolygon = new ArrayList<>(); - mCutoutTop.getBoundingPolygon(boundingPolygon); - - assertEquals(Arrays.asList( - new Point(75, 0), - new Point(50, 0), - new Point(75, 100), - new Point(50, 100)), boundingPolygon); + assertEquals(new Rect(50, 0, 75, 100), mCutoutTop.getBoundingRect()); } @Test @@ -167,104 +144,61 @@ public class DisplayCutoutTest { assertEquals(cutout.getSafeInsetRight(), 0); assertEquals(cutout.getSafeInsetBottom(), 0); - assertFalse(cutout.hasCutout()); + assertTrue(cutout.isEmpty()); } @Test public void inset_bounds() throws Exception { DisplayCutout cutout = mCutoutTop.inset(1, 2, 3, 4); - Rect boundingRect = new Rect(); - cutout.getBoundingRect(boundingRect); - - assertEquals(new Rect(49, -2, 74, 98), boundingRect); - - ArrayList<Point> boundingPolygon = new ArrayList<>(); - cutout.getBoundingPolygon(boundingPolygon); - - assertEquals(Arrays.asList( - new Point(74, -2), - new Point(49, -2), - new Point(74, 98), - new Point(49, 98)), boundingPolygon); + assertEquals(new Rect(49, -2, 74, 98), cutout.getBoundingRect()); } @Test public void calculateRelativeTo_top() throws Exception { DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(0, 0, 200, 400)); - Rect insets = new Rect(); - cutout.getSafeInsets(insets); - - assertEquals(new Rect(0, 100, 0, 0), insets); + assertEquals(new Rect(0, 100, 0, 0), cutout.getSafeInsets()); } @Test public void calculateRelativeTo_left() throws Exception { DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(0, 0, 400, 200)); - Rect insets = new Rect(); - cutout.getSafeInsets(insets); - - assertEquals(new Rect(75, 0, 0, 0), insets); + assertEquals(new Rect(75, 0, 0, 0), cutout.getSafeInsets()); } @Test public void calculateRelativeTo_bottom() throws Exception { DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(0, -300, 200, 100)); - Rect insets = new Rect(); - cutout.getSafeInsets(insets); - - assertEquals(new Rect(0, 0, 0, 100), insets); + assertEquals(new Rect(0, 0, 0, 100), cutout.getSafeInsets()); } @Test public void calculateRelativeTo_right() throws Exception { DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(-400, -200, 100, 100)); - Rect insets = new Rect(); - cutout.getSafeInsets(insets); - - assertEquals(new Rect(0, 0, 50, 0), insets); + assertEquals(new Rect(0, 0, 50, 0), cutout.getSafeInsets()); } @Test public void calculateRelativeTo_bounds() throws Exception { DisplayCutout cutout = mCutoutTop.calculateRelativeTo(new Rect(-1000, -2000, 100, 200)); - - Rect boundingRect = new Rect(); - cutout.getBoundingRect(boundingRect); - assertEquals(new Rect(1050, 2000, 1075, 2100), boundingRect); - - ArrayList<Point> boundingPolygon = new ArrayList<>(); - cutout.getBoundingPolygon(boundingPolygon); - - assertEquals(Arrays.asList( - new Point(1075, 2000), - new Point(1050, 2000), - new Point(1075, 2100), - new Point(1050, 2100)), boundingPolygon); + assertEquals(new Rect(1050, 2000, 1075, 2100), cutout.getBoundingRect()); } @Test public void fromBoundingPolygon() throws Exception { assertEquals( - new DisplayCutout( - new Rect(0, 0, 0, 0), // fromBoundingPolygon won't calculate safe insets. - new Rect(50, 0, 75, 100), - Arrays.asList( - new Point(75, 0), - new Point(50, 0), - new Point(75, 100), - new Point(50, 100))), + new Rect(50, 0, 75, 100), DisplayCutout.fromBoundingPolygon( Arrays.asList( new Point(75, 0), new Point(50, 0), new Point(75, 100), - new Point(50, 100)))); + new Point(50, 100))).getBounds().getBounds()); } @Test @@ -324,24 +258,12 @@ public class DisplayCutoutTest { } private static DisplayCutout createCutoutTop() { - return new DisplayCutout( - new Rect(0, 100, 0, 0), - new Rect(50, 0, 75, 100), - Arrays.asList( - new Point(75, 0), - new Point(50, 0), - new Point(75, 100), - new Point(50, 100))); + return createCutoutWithInsets(0, 100, 0, 0); } private static DisplayCutout createCutoutWithInsets(int left, int top, int right, int bottom) { return new DisplayCutout( new Rect(left, top, right, bottom), - new Rect(50, 0, 75, 100), - Arrays.asList( - new Point(75, 0), - new Point(50, 0), - new Point(75, 100), - new Point(50, 100))); + new Region(50, 0, 75, 100)); } } diff --git a/core/tests/coretests/src/android/view/ScaleGestureDetectorTest.java b/core/tests/coretests/src/android/view/ScaleGestureDetectorTest.java index 23d025164ed1..fba8eae6c3dd 100644 --- a/core/tests/coretests/src/android/view/ScaleGestureDetectorTest.java +++ b/core/tests/coretests/src/android/view/ScaleGestureDetectorTest.java @@ -17,6 +17,7 @@ package android.view; import android.content.Context; +import android.support.test.filters.LargeTest; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.ActivityInstrumentationTestCase2; @@ -36,6 +37,7 @@ import org.junit.runner.RunWith; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.Espresso.onView; +@LargeTest public class ScaleGestureDetectorTest extends ActivityInstrumentationTestCase2<ScaleGesture> { private ScaleGesture mScaleGestureActivity; diff --git a/core/tests/coretests/src/android/view/ViewAttachTest.java b/core/tests/coretests/src/android/view/ViewAttachTest.java index 44fcd13b5313..aa8f8d82e725 100644 --- a/core/tests/coretests/src/android/view/ViewAttachTest.java +++ b/core/tests/coretests/src/android/view/ViewAttachTest.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.graphics.PixelFormat; import android.os.SystemClock; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase2; import android.test.UiThreadTest; import android.view.View; @@ -28,6 +29,7 @@ import android.view.WindowManager; import com.android.frameworks.coretests.R; +@LargeTest public class ViewAttachTest extends ActivityInstrumentationTestCase2<ViewAttachTestActivity> { diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityCacheTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityCacheTest.java index 2f32d13693c7..4de8155663f5 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityCacheTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityCacheTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.support.test.filters.LargeTest; import android.support.test.runner.AndroidJUnit4; import android.view.View; @@ -42,7 +43,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; - +@LargeTest @RunWith(AndroidJUnit4.class) public class AccessibilityCacheTest { private static final int WINDOW_ID_1 = 0xBEEF; diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java index c1b2309ae64a..318d122b3f2f 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityInteractionClientTest.java @@ -22,6 +22,7 @@ import static org.mockito.MockitoAnnotations.initMocks; import android.os.Bundle; import android.os.RemoteException; +import android.support.test.filters.LargeTest; import android.support.test.runner.AndroidJUnit4; import libcore.util.EmptyArray; @@ -36,6 +37,7 @@ import java.util.List; /** * Tests for AccessibilityInteractionClient */ +@LargeTest @RunWith(AndroidJUnit4.class) public class AccessibilityInteractionClientTest { private static final int MOCK_CONNECTION_ID = 0xabcd; diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java index 79e6316ed614..b135025f6d21 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java @@ -18,6 +18,7 @@ package android.view.accessibility; import static org.junit.Assert.fail; +import android.support.test.filters.LargeTest; import android.support.test.runner.AndroidJUnit4; import android.util.ArraySet; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; @@ -29,6 +30,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; +@LargeTest @RunWith(AndroidJUnit4.class) public class AccessibilityNodeInfoTest { diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java index e3c91e64f69c..9edbf3efeebb 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java +++ b/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java @@ -26,6 +26,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.os.Bundle; import android.os.Parcel; +import android.support.test.filters.LargeTest; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; @@ -34,6 +35,7 @@ import com.android.frameworks.coretests.R; import org.junit.Test; import org.junit.runner.RunWith; +@LargeTest @RunWith(AndroidJUnit4.class) public class InputMethodInfoTest { diff --git a/core/tests/coretests/src/android/view/menu/MenuLayoutLandscapeTest.java b/core/tests/coretests/src/android/view/menu/MenuLayoutLandscapeTest.java index 9347b27bbf5d..8ed0d86d850d 100644 --- a/core/tests/coretests/src/android/view/menu/MenuLayoutLandscapeTest.java +++ b/core/tests/coretests/src/android/view/menu/MenuLayoutLandscapeTest.java @@ -21,8 +21,10 @@ import com.android.internal.view.menu.IconMenuView; import com.android.internal.view.menu.MenuBuilder; import android.content.pm.ActivityInfo; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase; +@LargeTest public class MenuLayoutLandscapeTest extends ActivityInstrumentationTestCase<MenuLayoutLandscape> { private static final String LONG_TITLE = "Really really really really really really really really really really long title"; private static final String SHORT_TITLE = "Item"; diff --git a/core/tests/coretests/src/android/view/menu/MenuLayoutPortraitTest.java b/core/tests/coretests/src/android/view/menu/MenuLayoutPortraitTest.java index b053699673aa..ccf12643593b 100644 --- a/core/tests/coretests/src/android/view/menu/MenuLayoutPortraitTest.java +++ b/core/tests/coretests/src/android/view/menu/MenuLayoutPortraitTest.java @@ -16,13 +16,12 @@ package android.view.menu; -import android.util.KeyUtils; -import com.android.internal.view.menu.IconMenuView; -import com.android.internal.view.menu.MenuBuilder; - import android.content.pm.ActivityInfo; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase; +import android.util.KeyUtils; +@LargeTest public class MenuLayoutPortraitTest extends ActivityInstrumentationTestCase<MenuLayoutPortrait> { private static final String LONG_TITLE = "Really really really really really really really really really really long title"; private static final String SHORT_TITLE = "Item"; diff --git a/core/tests/coretests/src/android/widget/DatePickerFocusTest.java b/core/tests/coretests/src/android/widget/DatePickerFocusTest.java index 513e40f4bcc3..be85450be429 100644 --- a/core/tests/coretests/src/android/widget/DatePickerFocusTest.java +++ b/core/tests/coretests/src/android/widget/DatePickerFocusTest.java @@ -19,6 +19,7 @@ package android.widget; import android.app.Activity; import android.app.Instrumentation; import android.os.SystemClock; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase2; import android.view.KeyEvent; import android.view.View; @@ -28,6 +29,7 @@ import com.android.frameworks.coretests.R; /** * Test {@link DatePicker} focus changes. */ +@LargeTest public class DatePickerFocusTest extends ActivityInstrumentationTestCase2<DatePickerActivity> { private Activity mActivity; diff --git a/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java b/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java index 28a3b67f7b0d..2add22105681 100644 --- a/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java +++ b/core/tests/coretests/src/android/widget/SelectionActionModeHelperTest.java @@ -22,6 +22,7 @@ import static java.util.function.Function.identity; import android.graphics.PointF; import android.graphics.RectF; +import android.support.test.filters.LargeTest; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,6 +32,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +@LargeTest @RunWith(JUnit4.class) public final class SelectionActionModeHelperTest { diff --git a/core/tests/coretests/src/android/widget/focus/HorizontalFocusSearchTest.java b/core/tests/coretests/src/android/widget/focus/HorizontalFocusSearchTest.java index b591e5f4f690..43986eed9b14 100644 --- a/core/tests/coretests/src/android/widget/focus/HorizontalFocusSearchTest.java +++ b/core/tests/coretests/src/android/widget/focus/HorizontalFocusSearchTest.java @@ -18,6 +18,7 @@ package android.widget.focus; import android.widget.focus.HorizontalFocusSearch; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase; import android.test.suitebuilder.annotation.Suppress; import android.widget.LinearLayout; @@ -32,6 +33,7 @@ import static android.widget.focus.VerticalFocusSearchTest.NewFocusSearchAlg; * various widths and vertical placements. */ // Suppress until bug http://b/issue?id=1416545 is fixed. +@LargeTest @Suppress public class HorizontalFocusSearchTest extends ActivityInstrumentationTestCase<HorizontalFocusSearch> { diff --git a/core/tests/coretests/src/android/widget/focus/VerticalFocusSearchTest.java b/core/tests/coretests/src/android/widget/focus/VerticalFocusSearchTest.java index f05d83abde5e..f01422ebfc52 100644 --- a/core/tests/coretests/src/android/widget/focus/VerticalFocusSearchTest.java +++ b/core/tests/coretests/src/android/widget/focus/VerticalFocusSearchTest.java @@ -18,6 +18,7 @@ package android.widget.focus; import android.widget.focus.VerticalFocusSearch; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase; import android.test.suitebuilder.annotation.Suppress; import android.view.FocusFinder; @@ -31,6 +32,7 @@ import android.widget.LinearLayout; * various widths and horizontal placements. */ // Suppress until bug http://b/issue?id=1416545 is fixed +@LargeTest @Suppress public class VerticalFocusSearchTest extends ActivityInstrumentationTestCase<VerticalFocusSearch> { diff --git a/core/tests/coretests/src/android/widget/listview/arrowscroll/ListWithFirstScreenUnSelectableTest.java b/core/tests/coretests/src/android/widget/listview/arrowscroll/ListWithFirstScreenUnSelectableTest.java index 400fd7d9247a..9a8e63421d61 100644 --- a/core/tests/coretests/src/android/widget/listview/arrowscroll/ListWithFirstScreenUnSelectableTest.java +++ b/core/tests/coretests/src/android/widget/listview/arrowscroll/ListWithFirstScreenUnSelectableTest.java @@ -16,12 +16,14 @@ package android.widget.listview.arrowscroll; -import android.widget.listview.ListWithFirstScreenUnSelectable; +import android.support.test.filters.LargeTest; import android.test.ActivityInstrumentationTestCase2; import android.view.KeyEvent; import android.widget.ListView; +import android.widget.listview.ListWithFirstScreenUnSelectable; import android.widget.AdapterView; +@LargeTest public class ListWithFirstScreenUnSelectableTest extends ActivityInstrumentationTestCase2<ListWithFirstScreenUnSelectable> { private ListView mListView; diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java new file mode 100644 index 000000000000..60416a720231 --- /dev/null +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -0,0 +1,665 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RawRes; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.NinePatchDrawable; + +import java.nio.ByteBuffer; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ArrayIndexOutOfBoundsException; +import java.lang.NullPointerException; +import java.lang.RuntimeException; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Class for decoding images as {@link Bitmap}s or {@link Drawable}s. + * @hide + */ +public final class ImageDecoder { + /** + * Source of the encoded image data. + */ + public static abstract class Source { + /* @hide */ + Resources getResources() { return null; } + + /* @hide */ + void close() {} + + /* @hide */ + abstract ImageDecoder createImageDecoder(); + }; + + private static class ByteArraySource extends Source { + ByteArraySource(byte[] data, int offset, int length) { + mData = data; + mOffset = offset; + mLength = length; + }; + private final byte[] mData; + private final int mOffset; + private final int mLength; + + @Override + public ImageDecoder createImageDecoder() { + return nCreate(mData, mOffset, mLength); + } + } + + private static class ByteBufferSource extends Source { + ByteBufferSource(ByteBuffer buffer) { + mBuffer = buffer; + } + private final ByteBuffer mBuffer; + + @Override + public ImageDecoder createImageDecoder() { + if (!mBuffer.isDirect() && mBuffer.hasArray()) { + int offset = mBuffer.arrayOffset() + mBuffer.position(); + int length = mBuffer.limit() - mBuffer.position(); + return nCreate(mBuffer.array(), offset, length); + } + return nCreate(mBuffer, mBuffer.position(), mBuffer.limit()); + } + } + + private static class ResourceSource extends Source { + ResourceSource(Resources res, int resId) + throws Resources.NotFoundException { + // Test that the resource can be found. + InputStream is = null; + try { + is = res.openRawResource(resId); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + + mResources = res; + mResId = resId; + } + + final Resources mResources; + final int mResId; + // This is just stored here in order to keep the underlying Asset + // alive. FIXME: Can I access the Asset (and keep it alive) without + // this object? + InputStream mInputStream; + + @Override + public Resources getResources() { return mResources; } + + @Override + public ImageDecoder createImageDecoder() { + // FIXME: Can I bypass creating the stream? + try { + mInputStream = mResources.openRawResource(mResId); + } catch (Resources.NotFoundException e) { + // This should never happen, since we already tested in the + // constructor. + } + if (!(mInputStream instanceof AssetManager.AssetInputStream)) { + // This should never happen. + throw new RuntimeException("Resource is not an asset?"); + } + long asset = ((AssetManager.AssetInputStream) mInputStream).getNativeAsset(); + return nCreate(asset); + } + + @Override + public void close() { + try { + mInputStream.close(); + } catch (IOException e) { + } finally { + mInputStream = null; + } + } + } + + /** + * Contains information about the encoded image. + */ + public static class ImageInfo { + public final int width; + public final int height; + // TODO?: Add more info? mimetype, ninepatch etc? + + ImageInfo(int width, int height) { + this.width = width; + this.height = height; + } + }; + + /** + * Used if the provided data is incomplete. + * + * There may be a partial image to display. + */ + public class IncompleteException extends Exception {}; + + /** + * Used if the provided data is corrupt. + * + * There may be a partial image to display. + */ + public class CorruptException extends Exception {}; + + /** + * Optional listener supplied to {@link #decodeDrawable} or + * {@link #decodeBitmap}. + */ + public static interface OnHeaderDecodedListener { + /** + * Called when the header is decoded and the size is known. + * + * @param info Information about the encoded image. + * @param decoder allows changing the default settings of the decode. + */ + public void onHeaderDecoded(ImageInfo info, ImageDecoder decoder); + + }; + + /** + * Optional listener supplied to the ImageDecoder. + */ + public static interface OnExceptionListener { + /** + * Called when there is a problem in the stream or in the data. + * FIXME: Or do not allow streams? + * FIXME: Report how much of the image has been decoded? + * + * @param e Exception containing information about the error. + * @return True to create and return a {@link Drawable}/ + * {@link Bitmap} with partial data. False to return + * {@code null}. True is the default. + */ + public boolean onException(Exception e); + }; + + // Fields + private long mNativePtr; + private final int mWidth; + private final int mHeight; + + private int mDesiredWidth; + private int mDesiredHeight; + private int mAllocator = DEFAULT_ALLOCATOR; + private boolean mRequireUnpremultiplied = false; + private boolean mMutable = false; + private boolean mPreferRamOverQuality = false; + private boolean mAsAlphaMask = false; + private Rect mCropRect; + + private PostProcess mPostProcess; + private OnExceptionListener mOnExceptionListener; + + + /** + * Private constructor called by JNI. {@link #recycle} must be + * called after decoding to delete native resources. + */ + @SuppressWarnings("unused") + private ImageDecoder(long nativePtr, int width, int height) { + mNativePtr = nativePtr; + mWidth = width; + mHeight = height; + mDesiredWidth = width; + mDesiredHeight = height; + } + + /** + * Create a new {@link Source} from an asset. + * + * @param res the {@link Resources} object containing the image data. + * @param resId resource ID of the image data. + * // FIXME: Can be an @DrawableRes? + * @return a new Source object, which can be passed to + * {@link #decodeDrawable} or {@link #decodeBitmap}. + * @throws Resources.NotFoundException if the asset does not exist. + */ + public static Source createSource(@NonNull Resources res, @RawRes int resId) + throws Resources.NotFoundException { + return new ResourceSource(res, resId); + } + + /** + * Create a new {@link Source} from a byte array. + * @param data byte array of compressed image data. + * @param offset offset into data for where the decoder should begin + * parsing. + * @param length number of bytes, beginning at offset, to parse. + * @throws NullPointerException if data is null. + * @throws ArrayIndexOutOfBoundsException if offset and length are + * not within data. + */ + // TODO: Overloads that don't use offset, length + public static Source createSource(@NonNull byte[] data, int offset, + int length) throws ArrayIndexOutOfBoundsException { + if (data == null) { + throw new NullPointerException("null byte[] in createSource!"); + } + if (offset < 0 || length < 0 || offset >= data.length || + offset + length > data.length) { + throw new ArrayIndexOutOfBoundsException( + "invalid offset/length!"); + } + return new ByteArraySource(data, offset, length); + } + + /** + * Create a new {@link Source} from a {@link java.nio.ByteBuffer}. + * + * The returned {@link Source} effectively takes ownership of the + * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after + * this call. + * + * Decoding will start from {@link java.nio.ByteBuffer#position()}. + */ + public static Source createSource(ByteBuffer buffer) { + return new ByteBufferSource(buffer); + } + + /** + * Return the width and height of a given sample size. + * + * This takes an input that functions like + * {@link BitmapFactory.Options#inSampleSize}. It returns a width and + * height that can be acheived by sampling the encoded image. Other widths + * and heights may be supported, but will require an additional (internal) + * scaling step. Such internal scaling is *not* supported with + * {@link #requireUnpremultiplied}. + * + * @param sampleSize Sampling rate of the encoded image. + * @return Point {@link Point#x} and {@link Point#y} correspond to the + * width and height after sampling. + */ + public Point getSampledSize(int sampleSize) { + if (sampleSize <= 0) { + throw new IllegalArgumentException("sampleSize must be positive! " + + "provided " + sampleSize); + } + if (mNativePtr == 0) { + throw new IllegalStateException("ImageDecoder is recycled!"); + } + + return nGetSampledSize(mNativePtr, sampleSize); + } + + // Modifiers + /** + * Resize the output to have the following size. + * + * @param width must be greater than 0. + * @param height must be greater than 0. + */ + public void resize(int width, int height) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Dimensions must be positive! " + + "provided (" + width + ", " + height + ")"); + } + + mDesiredWidth = width; + mDesiredHeight = height; + } + + /** + * Resize based on a sample size. + * + * This has the same effect as passing the result of + * {@link #getSampledSize} to {@link #resize(int, int)}. + * + * @param sampleSize Sampling rate of the encoded image. + */ + public void resize(int sampleSize) { + Point dimensions = this.getSampledSize(sampleSize); + this.resize(dimensions.x, dimensions.y); + } + + // These need to stay in sync with ImageDecoder.cpp's Allocator enum. + /** + * Use the default allocation for the pixel memory. + * + * Will typically result in a {@link Bitmap.Config#HARDWARE} + * allocation, but may be software for small images. In addition, this will + * switch to software when HARDWARE is incompatible, e.g. + * {@link #setMutable}, {@link #setAsAlphaMask}. + */ + public static final int DEFAULT_ALLOCATOR = 0; + + /** + * Use a software allocation for the pixel memory. + * + * Useful for drawing to a software {@link Canvas} or for + * accessing the pixels on the final output. + */ + public static final int SOFTWARE_ALLOCATOR = 1; + + /** + * Use shared memory for the pixel memory. + * + * Useful for sharing across processes. + */ + public static final int SHARED_MEMORY_ALLOCATOR = 2; + + /** + * Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}. + * + * This will throw an {@link java.lang.IllegalStateException} when combined + * with incompatible options, like {@link #setMutable} or + * {@link #setAsAlphaMask}. + */ + public static final int HARDWARE_ALLOCATOR = 3; + + /** @hide **/ + @Retention(SOURCE) + @IntDef({ DEFAULT_ALLOCATOR, SOFTWARE_ALLOCATOR, SHARED_MEMORY_ALLOCATOR, + HARDWARE_ALLOCATOR }) + public @interface Allocator {}; + + /** + * Choose the backing for the pixel memory. + * + * This is ignored for animated drawables. + * + * TODO: Allow accessing the backing from the Bitmap. + * + * @param allocator Type of allocator to use. + */ + public void setAllocator(@Allocator int allocator) { + if (allocator < DEFAULT_ALLOCATOR || allocator > HARDWARE_ALLOCATOR) { + throw new IllegalArgumentException("invalid allocator " + allocator); + } + mAllocator = allocator; + } + + /** + * Create a {@link Bitmap} with unpremultiplied pixels. + * + * By default, ImageDecoder will create a {@link Bitmap} with + * premultiplied pixels, which is required for drawing with the + * {@link android.view.View} system (i.e. to a {@link Canvas}). Calling + * this method will result in {@link #decodeBitmap} returning a + * {@link Bitmap} with unpremultiplied pixels. See + * {@link Bitmap#isPremultiplied}. Incompatible with + * {@link #decodeDrawable}; attempting to decode an unpremultiplied + * {@link Drawable} will throw an {@link java.lang.IllegalStateException}. + */ + public void requireUnpremultiplied() { + mRequireUnpremultiplied = true; + } + + /** + * Modify the image after decoding and scaling. + * + * This allows adding effects prior to returning a {@link Drawable} or + * {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap}, + * this is the only way to process the image after decoding. + * + * If set on a nine-patch image, the nine-patch data is ignored. + * + * For an animated image, the drawing commands drawn on the {@link Canvas} + * will be recorded immediately and then applied to each frame. + */ + public void setPostProcess(PostProcess p) { + mPostProcess = p; + } + + /** + * Set (replace) the {@link OnExceptionListener} on this object. + * + * Will be called if there is an error in the input. Without one, a + * partial {@link Bitmap} will be created. + */ + public void setOnExceptionListener(OnExceptionListener l) { + mOnExceptionListener = l; + } + + /** + * Crop the output to {@code subset} of the (possibly) scaled image. + * + * {@code subset} must be contained within the size set by {@link #resize} + * or the bounds of the image if resize was not called. Otherwise an + * {@link IllegalStateException} will be thrown. + * + * NOT intended as a replacement for + * {@link BitmapRegionDecoder#decodeRegion}. This supports all formats, + * but merely crops the output. + */ + public void crop(Rect subset) { + mCropRect = subset; + } + + /** + * Create a mutable {@link Bitmap}. + * + * By default, a {@link Bitmap} created will be immutable, but that can be + * changed with this call. + * + * Incompatible with {@link #HARDWARE_ALLOCATOR}, because + * {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable. Attempting to + * combine them will throw an {@link java.lang.IllegalStateException}. + * + * Incompatible with {@link #decodeDrawable}, which would require + * retrieving the Bitmap from the returned Drawable in order to modify. + * Attempting to decode a mutable {@link Drawable} will throw an + * {@link java.lang.IllegalStateException} + */ + public void setMutable() { + mMutable = true; + } + + /** + * Potentially save RAM at the expense of quality. + * + * This may result in a {@link Bitmap} with a denser {@link Bitmap.Config}, + * depending on the image. For example, for an opaque {@link Bitmap}, this + * may result in a {@link Bitmap.Config} with no alpha information. + */ + public void setPreferRamOverQuality() { + mPreferRamOverQuality = true; + } + + /** + * Potentially treat the output as an alpha mask. + * + * If the image is encoded in a format with only one channel, treat that + * channel as alpha. Otherwise this call has no effect. + * + * Incompatible with {@link #HARDWARE_ALLOCATOR}. Trying to combine them + * will throw an {@link java.lang.IllegalStateException}. + */ + public void setAsAlphaMask() { + mAsAlphaMask = true; + } + + /** + * Clean up resources. + * + * ImageDecoder has a private constructor, and will always be recycled + * by decodeDrawable or decodeBitmap which creates it, so there is no + * need for a finalizer. + */ + private void recycle() { + if (mNativePtr == 0) { + return; + } + nRecycle(mNativePtr); + mNativePtr = 0; + } + + private void checkState() { + if (mNativePtr == 0) { + throw new IllegalStateException("Cannot reuse ImageDecoder.Source!"); + } + + checkSubset(mDesiredWidth, mDesiredHeight, mCropRect); + + if (mAllocator == HARDWARE_ALLOCATOR) { + if (mMutable) { + throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!"); + } + if (mAsAlphaMask) { + throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!"); + } + } + + if (mPostProcess != null && mRequireUnpremultiplied) { + throw new IllegalStateException("Cannot draw to unpremultiplied pixels!"); + } + } + + private static void checkSubset(int width, int height, Rect r) { + if (r == null) { + return; + } + if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) { + throw new IllegalStateException("Subset " + r + " not contained by " + + "scaled image bounds: (" + width + " x " + height + ")"); + } + } + + /** + * Create a {@link Drawable}. + */ + public static Drawable decodeDrawable(Source src, OnHeaderDecodedListener listener) { + ImageDecoder decoder = src.createImageDecoder(); + if (decoder == null) { + return null; + } + + if (listener != null) { + ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight); + listener.onHeaderDecoded(info, decoder); + } + + decoder.checkState(); + + if (decoder.mRequireUnpremultiplied) { + // Though this could be supported (ignored) for opaque images, it + // seems better to always report this error. + throw new IllegalStateException("Cannot decode a Drawable with" + + " unpremultiplied pixels!"); + } + + if (decoder.mMutable) { + throw new IllegalStateException("Cannot decode a mutable Drawable!"); + } + + try { + Bitmap bm = nDecodeBitmap(decoder.mNativePtr, + decoder.mOnExceptionListener, + decoder.mPostProcess, + decoder.mDesiredWidth, decoder.mDesiredHeight, + decoder.mCropRect, + false, // decoder.mMutable + decoder.mAllocator, + false, // decoder.mRequireUnpremultiplied + decoder.mPreferRamOverQuality, + decoder.mAsAlphaMask + ); + if (bm == null) { + return null; + } + + Resources res = src.getResources(); + if (res == null) { + bm.setDensity(Bitmap.DENSITY_NONE); + } + + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + Rect opticalInsets = new Rect(); + bm.getOpticalInsets(opticalInsets); + Rect padding = new Rect(); + nGetPadding(decoder.mNativePtr, padding); + return new NinePatchDrawable(res, bm, np, padding, + opticalInsets, null); + } + + // TODO: Handle animation. + return new BitmapDrawable(res, bm); + } finally { + decoder.recycle(); + src.close(); + } + } + + /** + * Create a {@link Bitmap}. + */ + public static Bitmap decodeBitmap(Source src, OnHeaderDecodedListener listener) { + ImageDecoder decoder = src.createImageDecoder(); + if (decoder == null) { + return null; + } + + if (listener != null) { + ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight); + listener.onHeaderDecoded(info, decoder); + } + + decoder.checkState(); + + try { + return nDecodeBitmap(decoder.mNativePtr, + decoder.mOnExceptionListener, + decoder.mPostProcess, + decoder.mDesiredWidth, decoder.mDesiredHeight, + decoder.mCropRect, + decoder.mMutable, + decoder.mAllocator, + decoder.mRequireUnpremultiplied, + decoder.mPreferRamOverQuality, + decoder.mAsAlphaMask); + } finally { + decoder.recycle(); + src.close(); + } + } + + private static native ImageDecoder nCreate(long asset); + private static native ImageDecoder nCreate(ByteBuffer buffer, + int position, + int limit); + private static native ImageDecoder nCreate(byte[] data, int offset, + int length); + private static native Bitmap nDecodeBitmap(long nativePtr, + OnExceptionListener listener, + PostProcess postProcess, + int width, int height, + Rect cropRect, boolean mutable, + int allocator, boolean requireUnpremul, + boolean preferRamOverQuality, boolean asAlphaMask); + private static native Point nGetSampledSize(long nativePtr, + int sampleSize); + private static native void nGetPadding(long nativePtr, Rect outRect); + private static native void nRecycle(long nativePtr); +} diff --git a/graphics/java/android/graphics/PostProcess.java b/graphics/java/android/graphics/PostProcess.java new file mode 100644 index 000000000000..c5a31e828c97 --- /dev/null +++ b/graphics/java/android/graphics/PostProcess.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.graphics.drawable.Drawable; + + +/** + * Helper interface for adding custom processing to an image. + * + * The image being processed may be a {@link Drawable}, {@link Bitmap} or frame + * of an animated image produced by {@link ImageDecoder}. This is called before + * the requested object is returned. + * + * This custom processing also applies to image types that are otherwise + * immutable, such as {@link Bitmap.Config#HARDWARE}. + * + * On an animated image, the callback will only be called once, but the drawing + * commands will be applied to each frame, as if the {@code Canvas} had been + * returned by {@link Picture#beginRecording}. + * + * Supplied to ImageDecoder via {@link ImageDecoder#setPostProcess}. + * @hide + */ +public interface PostProcess { + /** + * Do any processing after (for example) decoding. + * + * Drawing to the {@link Canvas} will behave as if the initial processing + * (e.g. decoding) already exists in the Canvas. An implementation can draw + * effects on top of this, or it can even draw behind it using + * {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency + * to the corners to achieve rounded corners. That can be done with the + * following code: + * + * <code> + * Path path = new Path(); + * path.setFillType(Path.FillType.INVERSE_EVEN_ODD); + * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW); + * Paint paint = new Paint(); + * paint.setAntiAlias(true); + * paint.setColor(Color.TRANSPARENT); + * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + * canvas.drawPath(path, paint); + * return PixelFormat.TRANSLUCENT; + * </code> + * + * + * @param canvas The {@link Canvas} to draw to. + * @param width Width of {@code canvas}. Anything drawn outside of this + * will be ignored. + * @param height Height of {@code canvas}. Anything drawn outside of this + * will be ignored. + * @return Opacity of the result after drawing. + * {@link PixelFormat#UNKNOWN} means that the implementation did not + * change whether the image has alpha. Return this unless you added + * transparency (e.g. with the code above, in which case you should + * return {@code PixelFormat.TRANSLUCENT}) or you forced the image to + * be opaque (e.g. by drawing everywhere with an opaque color and + * {@code PorterDuff.Mode.DST_OVER}, in which case you should return + * {@code PixelFormat.OPAQUE}). + * {@link PixelFormat#TRANSLUCENT} means that the implementation added + * transparency. This is safe to return even if the image already had + * transparency. This is also safe to return if the result is opaque, + * though it may draw more slowly. + * {@link PixelFormat#OPAQUE} means that the implementation forced the + * image to be opaque. This is safe to return even if the image was + * already opaque. + * {@link PixelFormat#TRANSPARENT} (or any other integer) is not + * allowed, and will result in throwing an + * {@link java.lang.IllegalArgumentException}. + */ + @PixelFormat.Opacity + public int postProcess(@NonNull Canvas canvas, int width, int height); +} diff --git a/keystore/java/android/security/IKeyChainService.aidl b/keystore/java/android/security/IKeyChainService.aidl index eca52cc3e8b6..7c7417dfaaac 100644 --- a/keystore/java/android/security/IKeyChainService.aidl +++ b/keystore/java/android/security/IKeyChainService.aidl @@ -35,6 +35,7 @@ interface IKeyChainService { boolean generateKeyPair(in String algorithm, in ParcelableKeyGenParameterSpec spec); boolean attestKey(in String alias, in byte[] challenge, out KeymasterCertificateChain chain); + boolean setKeyPairCertificate(String alias, in byte[] userCert, in byte[] certChain); // APIs used by CertInstaller and DevicePolicyManager String installCaCertificate(in byte[] caCertificate); diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 3fe75cffaa71..5b95c81db24e 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -6880,6 +6880,9 @@ status_t ResTable::createIdmap(const ResTable& overlay, return UNKNOWN_ERROR; } + // The number of resources overlaid that were not explicitly marked overlayable. + size_t forcedOverlayCount = 0u; + KeyedVector<uint8_t, IdmapTypeMap> map; // overlaid packages are assumed to contain only one package group @@ -6919,6 +6922,7 @@ status_t ResTable::createIdmap(const ResTable& overlay, continue; } + uint32_t typeSpecFlags = 0u; const String16 overlayType(resName.type, resName.typeLen); const String16 overlayName(resName.name, resName.nameLen); uint32_t overlayResID = overlay.identifierForName(overlayName.string(), @@ -6926,14 +6930,23 @@ status_t ResTable::createIdmap(const ResTable& overlay, overlayType.string(), overlayType.size(), overlayPackage.string(), - overlayPackage.size()); + overlayPackage.size(), + &typeSpecFlags); if (overlayResID == 0) { + // No such target resource was found. if (typeMap.entryMap.isEmpty()) { typeMap.entryOffset++; } continue; } + // Now that we know this is being overlaid, check if it can be, and emit a warning if + // it can't. + if ((dtohl(typeConfigs->typeSpecFlags[entryIndex]) & + ResTable_typeSpec::SPEC_OVERLAYABLE) == 0) { + forcedOverlayCount++; + } + if (typeMap.overlayTypeId == -1) { typeMap.overlayTypeId = Res_GETTYPE(overlayResID) + 1; } @@ -7012,6 +7025,10 @@ status_t ResTable::createIdmap(const ResTable& overlay, typeData += entryCount * 2; } + if (forcedOverlayCount > 0) { + ALOGW("idmap: overlaid %zu resources not marked overlayable", forcedOverlayCount); + } + return NO_ERROR; } diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 20d017813cf7..8cf4de9167e5 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -1339,9 +1339,13 @@ struct ResTable_typeSpec // Number of uint32_t entry configuration masks that follow. uint32_t entryCount; - enum { + enum : uint32_t { // Additional flag indicating an entry is public. - SPEC_PUBLIC = 0x40000000 + SPEC_PUBLIC = 0x40000000u, + + // Additional flag indicating an entry is overlayable at runtime. + // Added in Android-P. + SPEC_OVERLAYABLE = 0x80000000u, }; }; diff --git a/libs/androidfw/tests/data/basic/basic.apk b/libs/androidfw/tests/data/basic/basic.apk Binary files differindex 0c17328e86b2..18ef75e91ded 100644 --- a/libs/androidfw/tests/data/basic/basic.apk +++ b/libs/androidfw/tests/data/basic/basic.apk diff --git a/libs/androidfw/tests/data/basic/basic_de_fr.apk b/libs/androidfw/tests/data/basic/basic_de_fr.apk Binary files differindex e45258c6a005..767dff6fcfa5 100644 --- a/libs/androidfw/tests/data/basic/basic_de_fr.apk +++ b/libs/androidfw/tests/data/basic/basic_de_fr.apk diff --git a/libs/androidfw/tests/data/basic/basic_hdpi-v4.apk b/libs/androidfw/tests/data/basic/basic_hdpi-v4.apk Binary files differindex 4ae1a7c87a70..58953f56d736 100644 --- a/libs/androidfw/tests/data/basic/basic_hdpi-v4.apk +++ b/libs/androidfw/tests/data/basic/basic_hdpi-v4.apk diff --git a/libs/androidfw/tests/data/basic/basic_xhdpi-v4.apk b/libs/androidfw/tests/data/basic/basic_xhdpi-v4.apk Binary files differindex a240d4c06c1d..103f6565bb06 100644 --- a/libs/androidfw/tests/data/basic/basic_xhdpi-v4.apk +++ b/libs/androidfw/tests/data/basic/basic_xhdpi-v4.apk diff --git a/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk b/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk Binary files differindex fd3d9b233084..61369d506786 100644 --- a/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk +++ b/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk diff --git a/libs/androidfw/tests/data/basic/build b/libs/androidfw/tests/data/basic/build index d619800d6fc5..5682ed4d3041 100755 --- a/libs/androidfw/tests/data/basic/build +++ b/libs/androidfw/tests/data/basic/build @@ -19,11 +19,15 @@ set -e PATH_TO_FRAMEWORK_RES=${ANDROID_BUILD_TOP}/prebuilts/sdk/current/android.jar -aapt package \ - -M AndroidManifest.xml \ - -S res \ - -A assets \ +aapt2 compile --dir res -o compiled.flata +aapt2 link \ -I $PATH_TO_FRAMEWORK_RES \ - --split hdpi --split xhdpi --split xxhdpi --split fr,de \ - -F basic.apk \ - -f + --manifest AndroidManifest.xml \ + -A assets \ + --split basic_hdpi-v4.apk:hdpi \ + --split basic_xhdpi-v4.apk:xhdpi \ + --split basic_xxhdpi-v4.apk:xxhdpi \ + --split basic_de_fr.apk:de,fr \ + -o basic.apk \ + compiled.flata +rm compiled.flata diff --git a/libs/androidfw/tests/data/basic/res/values/values.xml b/libs/androidfw/tests/data/basic/res/values/values.xml index 638c9832ab4c..6c474596b5cd 100644 --- a/libs/androidfw/tests/data/basic/res/values/values.xml +++ b/libs/androidfw/tests/data/basic/res/values/values.xml @@ -60,4 +60,9 @@ <item>2</item> <item>3</item> </integer-array> + + <overlayable> + <item type="string" name="test2" /> + <item type="array" name="integerArray1" /> + </overlayable> </resources> diff --git a/libs/androidfw/tests/data/overlay/build b/libs/androidfw/tests/data/overlay/build index 112f373ead30..716b1bd9db64 100755 --- a/libs/androidfw/tests/data/overlay/build +++ b/libs/androidfw/tests/data/overlay/build @@ -17,4 +17,6 @@ set -e -aapt package -M AndroidManifest.xml -S res -F overlay.apk -f +aapt2 compile --dir res -o compiled.flata +aapt2 link --manifest AndroidManifest.xml -o overlay.apk compiled.flata +rm compiled.flata diff --git a/libs/androidfw/tests/data/overlay/overlay.apk b/libs/androidfw/tests/data/overlay/overlay.apk Binary files differindex 40bf17c5951a..33f961117c44 100644 --- a/libs/androidfw/tests/data/overlay/overlay.apk +++ b/libs/androidfw/tests/data/overlay/overlay.apk diff --git a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java index a8262c8cc4c8..974b2a4389e2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java +++ b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java @@ -271,7 +271,7 @@ public class ZoneGetter { * @param now The current time, used to tell whether daylight savings is active. * @return A CharSequence suitable for display as the offset label of {@code tz}. */ - private static CharSequence getGmtOffsetText(TimeZoneFormat tzFormatter, Locale locale, + public static CharSequence getGmtOffsetText(TimeZoneFormat tzFormatter, Locale locale, TimeZone tz, Date now) { final SpannableStringBuilder builder = new SpannableStringBuilder(); diff --git a/packages/SystemUI/res/drawable/ic_cast.xml b/packages/SystemUI/res/drawable/ic_cast.xml new file mode 100644 index 000000000000..b86dfea07682 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_cast.xml @@ -0,0 +1,31 @@ +<!-- + Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/colorControlNormal"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M1 18v2c0 .55 .45 1 1 1h2c0-1.66-1.34-3-3-3zm0-2.94c-.01 .51 .32 .93 .82 1.02 +2.08 .36 3.74 2 4.1 4.08 .09 .48 .5 .84 .99 .84 .61 0 1.09-.54 1-1.14a6.996 +6.996 0 0 0-5.8-5.78c-.59-.09-1.09 .38 -1.11 .98 zm0-4.03c-.01 .52 .34 .96 .85 +1.01 4.26 .43 7.68 3.82 8.1 8.08 .05 .5 .48 .88 .99 .88 .59 0 1.06-.51 +1-1.1-.52-5.21-4.66-9.34-9.87-9.85-.57-.05-1.05 .4 -1.07 .98 zM21 3H3c-1.1 0-2 +.9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_speaker.xml b/packages/SystemUI/res/drawable/ic_speaker.xml new file mode 100644 index 000000000000..1ea293c0b690 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_speaker.xml @@ -0,0 +1,26 @@ +<!-- + Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/colorControlNormal" > + <path + android:pathData="M17,2L7,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,1.99 2,1.99L17,22c1.1,0 2,-0.9 2,-2L19,4c0,-1.1 -0.9,-2 -2,-2zM12,4c1.1,0 2,0.9 2,2s-0.9,2 -2,2c-1.11,0 -2,-0.9 -2,-2s0.89,-2 2,-2zM12,20c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" + android:fillColor="#FFFFFFFF"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_speaker_group.xml b/packages/SystemUI/res/drawable/ic_speaker_group.xml new file mode 100644 index 000000000000..d6867d7265b0 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_speaker_group.xml @@ -0,0 +1,32 @@ +<!-- + Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/colorControlNormal" > + <path + android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z" + android:fillColor="#FFFFFFFF"/> + <path + android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" + android:fillColor="#FFFFFFFF"/> + <path + android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z" + android:fillColor="#FFFFFFFF"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_tv.xml b/packages/SystemUI/res/drawable/ic_tv.xml new file mode 100644 index 000000000000..cc2ae910ae88 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_tv.xml @@ -0,0 +1,26 @@ +<!-- + Copyright (C) 2017 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:tint="?android:attr/colorControlNormal" > + <path + android:pathData="M21,3L3,3c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h5v2h8v-2h5c1.1,0 1.99,-0.9 1.99,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,17L3,17L3,5h18v12z" + android:fillColor="#FFFFFFFF"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/output_chooser.xml b/packages/SystemUI/res/layout/output_chooser.xml index 22c3bcf6b9d7..3d0ab3599bf1 100644 --- a/packages/SystemUI/res/layout/output_chooser.xml +++ b/packages/SystemUI/res/layout/output_chooser.xml @@ -19,6 +19,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:sysui="http://schemas.android.com/apk/res-auto" android:id="@+id/output_chooser" + android:minWidth="320dp" + android:minHeight="320dp" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dp" > @@ -39,12 +41,6 @@ android:gravity="center" android:orientation="vertical"> - <ImageView - android:id="@android:id/icon" - android:layout_width="56dp" - android:layout_height="56dp" - android:tint="?android:attr/textColorSecondary" /> - <TextView android:id="@android:id/title" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 78e621e4b559..fd205dd55662 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1269,6 +1269,14 @@ <string name="volume_dialog_accessibility_shown_message">%s volume controls shown. Swipe up to dismiss.</string> <string name="volume_dialog_accessibility_dismissed_message">Volume controls hidden</string> + <string name="output_title">Media output</string> + <string name="output_calls_title">Phone call output</string> + <string name="output_none_found">No devices found</string> + <string name="output_none_found_service_off">No devices found. Try turning on <xliff:g id="service" example="Bluetooth">%1$s</xliff:g></string> + <string name="output_service_bt">Bluetooth</string> + <string name="output_service_wifi">Wi-Fi</string> + <string name="output_service_bt_wifi">Bluetooth and Wi-Fi</string> + <!-- Name of special SystemUI debug settings --> <string name="system_ui_tuner">System UI Tuner</string> diff --git a/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java b/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java index edd1748c7380..6aa465ce9f8c 100644 --- a/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java +++ b/packages/SystemUI/src/com/android/systemui/EmulatedDisplayCutout.java @@ -24,6 +24,7 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Point; +import android.graphics.Region; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; @@ -36,7 +37,8 @@ import android.view.ViewGroup.LayoutParams; import android.view.WindowInsets; import android.view.WindowManager; -import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Emulates a display cutout by drawing its shape in an overlay as supplied by @@ -85,6 +87,7 @@ public class EmulatedDisplayCutout extends SystemUI { PixelFormat.TRANSLUCENT); lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS | WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY; + lp.flags2 |= WindowManager.LayoutParams.FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA; lp.setTitle("EmulatedDisplayCutout"); lp.gravity = Gravity.TOP; return lp; @@ -102,9 +105,8 @@ public class EmulatedDisplayCutout extends SystemUI { }; private static class CutoutView extends View { - private Paint mPaint = new Paint(); - private Path mPath = new Path(); - private ArrayList<Point> mBoundingPolygon = new ArrayList<>(); + private final Paint mPaint = new Paint(); + private final Path mBounds = new Path(); CutoutView(Context context) { super(context); @@ -112,28 +114,22 @@ public class EmulatedDisplayCutout extends SystemUI { @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { - insets.getDisplayCutout().getBoundingPolygon(mBoundingPolygon); + if (insets.getDisplayCutout() != null) { + insets.getDisplayCutout().getBounds().getBoundaryPath(mBounds); + } else { + mBounds.reset(); + } invalidate(); - return insets.consumeCutout(); + return insets.consumeDisplayCutout(); } @Override protected void onDraw(Canvas canvas) { - if (!mBoundingPolygon.isEmpty()) { + if (!mBounds.isEmpty()) { mPaint.setColor(Color.DKGRAY); mPaint.setStyle(Paint.Style.FILL); - mPath.reset(); - for (int i = 0; i < mBoundingPolygon.size(); i++) { - Point point = mBoundingPolygon.get(i); - if (i == 0) { - mPath.moveTo(point.x, point.y); - } else { - mPath.lineTo(point.x, point.y); - } - } - mPath.close(); - canvas.drawPath(mPath, mPaint); + canvas.drawPath(mBounds, mPaint); } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java index fa82e3364b1f..f8843a997d59 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java +++ b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserDialog.java @@ -16,6 +16,10 @@ package com.android.systemui.volume; +import static android.support.v7.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTED; +import static android.support.v7.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTING; +import static android.support.v7.media.MediaRouter.UNSELECT_REASON_DISCONNECTED; + import static com.android.settingslib.bluetooth.Utils.getBtClassDrawableWithDescription; import android.bluetooth.BluetoothClass; @@ -27,7 +31,15 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.net.wifi.WifiManager; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteSelector; +import android.support.v7.media.MediaRouter; import android.util.Log; import android.util.Pair; @@ -38,8 +50,13 @@ import com.android.systemui.R; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.BluetoothController; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; public class OutputChooserDialog extends SystemUIDialog implements DialogInterface.OnDismissListener, OutputChooserLayout.Callback { @@ -47,15 +64,33 @@ public class OutputChooserDialog extends SystemUIDialog private static final String TAG = Util.logTag(OutputChooserDialog.class); private static final int MAX_DEVICES = 10; + private static final long UPDATE_DELAY_MS = 300L; + static final int MSG_UPDATE_ITEMS = 1; + private final Context mContext; private final BluetoothController mController; + private final WifiManager mWifiManager; private OutputChooserLayout mView; + private final MediaRouter mRouter; + private final MediaRouterCallback mRouterCallback; + private long mLastUpdateTime; + private final MediaRouteSelector mRouteSelector; + private Drawable mDefaultIcon; + private Drawable mTvIcon; + private Drawable mSpeakerIcon; + private Drawable mSpeakerGroupIcon; public OutputChooserDialog(Context context) { super(context); mContext = context; mController = Dependency.get(BluetoothController.class); + mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + mRouter = MediaRouter.getInstance(context); + mRouterCallback = new MediaRouterCallback(); + mRouteSelector = new MediaRouteSelector.Builder() + .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) + .build(); final IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); context.registerReceiver(mReceiver, filter); @@ -67,10 +102,21 @@ public class OutputChooserDialog extends SystemUIDialog setContentView(R.layout.output_chooser); setCanceledOnTouchOutside(true); setOnDismissListener(this::onDismiss); + setTitle(R.string.output_title); + mView = findViewById(R.id.output_chooser); mView.setCallback(this); - updateItems(); - mController.addCallback(mCallback); + + mDefaultIcon = mContext.getDrawable(R.drawable.ic_cast); + mTvIcon = mContext.getDrawable(R.drawable.ic_tv); + mSpeakerIcon = mContext.getDrawable(R.drawable.ic_speaker); + mSpeakerGroupIcon = mContext.getDrawable(R.drawable.ic_speaker_group); + + final boolean wifiOff = !mWifiManager.isWifiEnabled(); + final boolean btOff = !mController.isBluetoothEnabled(); + if (wifiOff || btOff) { + mView.setEmptyState(getDisabledServicesMessage(wifiOff, btOff)); + } } protected void cleanUp() {} @@ -82,43 +128,97 @@ public class OutputChooserDialog extends SystemUIDialog } @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + mRouter.addCallback(mRouteSelector, mRouterCallback, + MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN); + mController.addCallback(mCallback); + } + + @Override + public void onDetachedFromWindow() { + mRouter.removeCallback(mRouterCallback); + mController.removeCallback(mCallback); + super.onDetachedFromWindow(); + } + + @Override public void onDismiss(DialogInterface unused) { mContext.unregisterReceiver(mReceiver); - mController.removeCallback(mCallback); cleanUp(); } @Override public void onDetailItemClick(OutputChooserLayout.Item item) { if (item == null || item.tag == null) return; - final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; - if (device != null && device.getMaxConnectionState() - == BluetoothProfile.STATE_DISCONNECTED) { - mController.connect(device); + if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) { + final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; + if (device != null && device.getMaxConnectionState() + == BluetoothProfile.STATE_DISCONNECTED) { + mController.connect(device); + } + } else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) { + final MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.tag; + if (route.isEnabled()) { + route.select(); + } } } @Override public void onDetailItemDisconnect(OutputChooserLayout.Item item) { if (item == null || item.tag == null) return; - final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; - if (device != null) { - mController.disconnect(device); + if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_BT) { + final CachedBluetoothDevice device = (CachedBluetoothDevice) item.tag; + if (device != null) { + mController.disconnect(device); + } + } else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) { + mRouter.unselect(UNSELECT_REASON_DISCONNECTED); } } private void updateItems() { - if (mView == null) return; - if (mController.isBluetoothEnabled()) { - mView.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty, - R.string.quick_settings_bluetooth_detail_empty_text); - mView.setItemsVisible(true); - } else { - mView.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty, - R.string.bt_is_off); - mView.setItemsVisible(false); + if (SystemClock.uptimeMillis() - mLastUpdateTime < UPDATE_DELAY_MS) { + mHandler.removeMessages(MSG_UPDATE_ITEMS); + mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ITEMS), + mLastUpdateTime + UPDATE_DELAY_MS); + return; } + mLastUpdateTime = SystemClock.uptimeMillis(); + if (mView == null) return; ArrayList<OutputChooserLayout.Item> items = new ArrayList<>(); + + // Add bluetooth devices + addBluetoothDevices(items); + + // Add remote displays + addRemoteDisplayRoutes(items); + + Collections.sort(items, ItemComparator.sInstance); + + if (items.size() == 0) { + String emptyMessage = mContext.getString(R.string.output_none_found); + final boolean wifiOff = !mWifiManager.isWifiEnabled(); + final boolean btOff = !mController.isBluetoothEnabled(); + if (wifiOff || btOff) { + emptyMessage = getDisabledServicesMessage(wifiOff, btOff); + } + mView.setEmptyState(emptyMessage); + } + + mView.setItems(items.toArray(new OutputChooserLayout.Item[items.size()])); + } + + private String getDisabledServicesMessage(boolean wifiOff, boolean btOff) { + return mContext.getString(R.string.output_none_found_service_off, + wifiOff && btOff ? mContext.getString(R.string.output_service_bt_wifi) + : wifiOff ? mContext.getString(R.string.output_service_wifi) + : mContext.getString(R.string.output_service_bt)); + } + + private void addBluetoothDevices(List<OutputChooserLayout.Item> items) { final Collection<CachedBluetoothDevice> devices = mController.getDevices(); if (devices != null) { int connectedDevices = 0; @@ -134,6 +234,7 @@ public class OutputChooserDialog extends SystemUIDialog item.iconResId = R.drawable.ic_qs_bluetooth_on; item.line1 = device.getName(); item.tag = device; + item.deviceType = OutputChooserLayout.Item.DEVICE_TYPE_BT; int state = device.getMaxConnectionState(); if (state == BluetoothProfile.STATE_CONNECTED) { item.iconResId = R.drawable.ic_qs_bluetooth_connected; @@ -163,7 +264,87 @@ public class OutputChooserDialog extends SystemUIDialog } } } - mView.setItems(items.toArray(new OutputChooserLayout.Item[items.size()])); + } + + private void addRemoteDisplayRoutes(List<OutputChooserLayout.Item> items) { + List<MediaRouter.RouteInfo> routes = mRouter.getRoutes(); + for(MediaRouter.RouteInfo route : routes) { + if (route.isDefaultOrBluetooth() || !route.isEnabled() + || !route.matchesSelector(mRouteSelector)) { + continue; + } + final OutputChooserLayout.Item item = new OutputChooserLayout.Item(); + item.icon = getIconDrawable(route); + item.line1 = route.getName(); + item.tag = route; + item.deviceType = OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER; + if (route.getConnectionState() == CONNECTION_STATE_CONNECTING) { + mContext.getString(R.string.quick_settings_connecting); + } else { + item.line2 = route.getDescription(); + } + + if (route.getConnectionState() == CONNECTION_STATE_CONNECTED) { + item.canDisconnect = true; + } + items.add(item); + } + } + + private Drawable getIconDrawable(MediaRouter.RouteInfo route) { + Uri iconUri = route.getIconUri(); + if (iconUri != null) { + try { + InputStream is = getContext().getContentResolver().openInputStream(iconUri); + Drawable drawable = Drawable.createFromStream(is, null); + if (drawable != null) { + return drawable; + } + } catch (IOException e) { + Log.w(TAG, "Failed to load " + iconUri, e); + // Falls back. + } + } + return getDefaultIconDrawable(route); + } + + private Drawable getDefaultIconDrawable(MediaRouter.RouteInfo route) { + // If the type of the receiver device is specified, use it. + switch (route.getDeviceType()) { + case MediaRouter.RouteInfo.DEVICE_TYPE_TV: + return mTvIcon; + case MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER: + return mSpeakerIcon; + } + + // Otherwise, make the best guess based on other route information. + if (route instanceof MediaRouter.RouteGroup) { + // Only speakers can be grouped for now. + return mSpeakerGroupIcon; + } + return mDefaultIcon; + } + + private final class MediaRouterCallback extends MediaRouter.Callback { + @Override + public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { + updateItems(); + } + + @Override + public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { + updateItems(); + } + + @Override + public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { + updateItems(); + } + + @Override + public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) { + dismiss(); + } } private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -188,4 +369,33 @@ public class OutputChooserDialog extends SystemUIDialog updateItems(); } }; + + static final class ItemComparator implements Comparator<OutputChooserLayout.Item> { + public static final ItemComparator sInstance = new ItemComparator(); + + @Override + public int compare(OutputChooserLayout.Item lhs, OutputChooserLayout.Item rhs) { + // Connected item(s) first + if (lhs.canDisconnect != rhs.canDisconnect) { + return Boolean.compare(rhs.canDisconnect, lhs.canDisconnect); + } + // Bluetooth items before media routes + if (lhs.deviceType != rhs.deviceType) { + return Integer.compare(lhs.deviceType, rhs.deviceType); + } + // then by name + return lhs.line1.toString().compareToIgnoreCase(rhs.line1.toString()); + } + } + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + switch (message.what) { + case MSG_UPDATE_ITEMS: + updateItems(); + break; + } + } + }; }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserLayout.java b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserLayout.java index e8be4fd553a1..22ced60006b0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/OutputChooserLayout.java +++ b/packages/SystemUI/src/com/android/systemui/volume/OutputChooserLayout.java @@ -55,7 +55,6 @@ public class OutputChooserLayout extends FrameLayout { private AutoSizingList mItemList; private View mEmpty; private TextView mEmptyText; - private ImageView mEmptyIcon; private Item[] mItems; @@ -76,7 +75,6 @@ public class OutputChooserLayout extends FrameLayout { mEmpty = findViewById(android.R.id.empty); mEmpty.setVisibility(GONE); mEmptyText = mEmpty.findViewById(android.R.id.title); - mEmptyIcon = mEmpty.findViewById(android.R.id.icon); } @Override @@ -93,9 +91,8 @@ public class OutputChooserLayout extends FrameLayout { } } - public void setEmptyState(int icon, int text) { + public void setEmptyState(String text) { mEmpty.post(() -> { - mEmptyIcon.setImageResource(icon); mEmptyText.setText(text); }); } @@ -241,6 +238,8 @@ public class OutputChooserLayout extends FrameLayout { } public static class Item { + public static int DEVICE_TYPE_BT = 1; + public static int DEVICE_TYPE_MEDIA_ROUTER = 2; public int iconResId; public Drawable icon; public Drawable overlay; @@ -249,6 +248,7 @@ public class OutputChooserLayout extends FrameLayout { public Object tag; public boolean canDisconnect; public int icon2 = -1; + public int deviceType = 0; } public interface Callback { diff --git a/services/backup/java/com/android/server/backup/TransportManager.java b/services/backup/java/com/android/server/backup/TransportManager.java index f1854433dcd0..fbdb18327563 100644 --- a/services/backup/java/com/android/server/backup/TransportManager.java +++ b/services/backup/java/com/android/server/backup/TransportManager.java @@ -483,6 +483,7 @@ public class TransportManager { description.currentDestinationString = currentDestinationString; description.dataManagementIntent = dataManagementIntent; description.dataManagementLabel = dataManagementLabel; + Slog.d(TAG, "Transport " + name + " updated its attributes"); } } diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 0ffc77923f1a..31aea63875ea 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -1406,7 +1406,7 @@ public class AccountManagerService mLocalUnlockedUsers.put(userId, true); } if (userId < 1) return; - syncSharedAccounts(userId); + mHandler.post(() -> syncSharedAccounts(userId)); } private void syncSharedAccounts(int userId) { diff --git a/services/core/java/com/android/server/location/ContextHubService.java b/services/core/java/com/android/server/location/ContextHubService.java index 7f88663d4e3b..26edf6270770 100644 --- a/services/core/java/com/android/server/location/ContextHubService.java +++ b/services/core/java/com/android/server/location/ContextHubService.java @@ -79,7 +79,8 @@ public class ContextHubService extends IContextHubService.Stub { private final Context mContext; - private final ContextHubInfo[] mContextHubInfo; + private final Map<Integer, ContextHubInfo> mContextHubIdToInfoMap; + private final List<ContextHubInfo> mContextHubInfoList; private final RemoteCallbackList<IContextHubCallback> mCallbacksList = new RemoteCallbackList<>(); @@ -141,8 +142,9 @@ public class ContextHubService extends IContextHubService.Stub { if (mContextHubProxy == null) { mTransactionManager = null; mClientManager = null; - mDefaultClientMap = Collections.EMPTY_MAP; - mContextHubInfo = new ContextHubInfo[0]; + mDefaultClientMap = Collections.emptyMap(); + mContextHubIdToInfoMap = Collections.emptyMap(); + mContextHubInfoList = Collections.emptyList(); return; } @@ -157,20 +159,16 @@ public class ContextHubService extends IContextHubService.Stub { Log.e(TAG, "RemoteException while getting Context Hub info", e); hubList = Collections.emptyList(); } - mContextHubInfo = ContextHubServiceUtil.createContextHubInfoArray(hubList); + mContextHubIdToInfoMap = Collections.unmodifiableMap( + ContextHubServiceUtil.createContextHubInfoMap(hubList)); + mContextHubInfoList = new ArrayList<>(mContextHubIdToInfoMap.values()); HashMap<Integer, IContextHubClient> defaultClientMap = new HashMap<>(); - for (ContextHubInfo contextHubInfo : mContextHubInfo) { - int contextHubId = contextHubInfo.getId(); - + for (int contextHubId : mContextHubIdToInfoMap.keySet()) { IContextHubClient client = mClientManager.registerClient( createDefaultClientCallback(contextHubId), contextHubId); defaultClientMap.put(contextHubId, client); - } - mDefaultClientMap = Collections.unmodifiableMap(defaultClientMap); - for (ContextHubInfo contextHubInfo : mContextHubInfo) { - int contextHubId = contextHubInfo.getId(); try { mContextHubProxy.registerCallback( contextHubId, new ContextHubServiceCallback(contextHubId)); @@ -178,18 +176,12 @@ public class ContextHubService extends IContextHubService.Stub { Log.e(TAG, "RemoteException while registering service callback for hub (ID = " + contextHubId + ")", e); } - } - - // Do a query to initialize the service cache list of nanoapps - // TODO(b/69270990): Remove this when old API is deprecated - for (ContextHubInfo contextHubInfo : mContextHubInfo) { - queryNanoAppsInternal(contextHubInfo.getId()); - } - for (int i = 0; i < mContextHubInfo.length; i++) { - Log.d(TAG, "ContextHub[" + i + "] id: " + mContextHubInfo[i].getId() - + ", name: " + mContextHubInfo[i].getName()); + // Do a query to initialize the service cache list of nanoapps + // TODO(b/69270990): Remove this when old API is deprecated + queryNanoAppsInternal(contextHubId); } + mDefaultClientMap = Collections.unmodifiableMap(defaultClientMap); } /** @@ -267,23 +259,29 @@ public class ContextHubService extends IContextHubService.Stub { @Override public int[] getContextHubHandles() throws RemoteException { checkPermissions(); - int[] returnArray = new int[mContextHubInfo.length]; - - for (int i = 0; i < returnArray.length; ++i) { - returnArray[i] = i; - } - return returnArray; + return ContextHubServiceUtil.createPrimitiveIntArray(mContextHubIdToInfoMap.keySet()); } @Override - public ContextHubInfo getContextHubInfo(int contextHubId) throws RemoteException { + public ContextHubInfo getContextHubInfo(int contextHubHandle) throws RemoteException { checkPermissions(); - if (!(contextHubId >= 0 && contextHubId < mContextHubInfo.length)) { - Log.e(TAG, "Invalid context hub handle " + contextHubId); - return null; // null means fail + if (!mContextHubIdToInfoMap.containsKey(contextHubHandle)) { + Log.e(TAG, "Invalid Context Hub handle " + contextHubHandle + " in getContextHubInfo"); + return null; } - return mContextHubInfo[contextHubId]; + return mContextHubIdToInfoMap.get(contextHubHandle); + } + + /** + * Returns a List of ContextHubInfo object describing the available hubs. + * + * @return the List of ContextHubInfo objects + */ + @Override + public List<ContextHubInfo> getContextHubs() throws RemoteException { + checkPermissions(); + return mContextHubInfoList; } /** @@ -347,28 +345,27 @@ public class ContextHubService extends IContextHubService.Stub { } @Override - public int loadNanoApp(int contextHubId, NanoApp app) throws RemoteException { + public int loadNanoApp(int contextHubHandle, NanoApp nanoApp) throws RemoteException { checkPermissions(); if (mContextHubProxy == null) { return -1; } - - if (!(contextHubId >= 0 && contextHubId < mContextHubInfo.length)) { - Log.e(TAG, "Invalid contextHubhandle " + contextHubId); + if (!isValidContextHubId(contextHubHandle)) { + Log.e(TAG, "Invalid Context Hub handle " + contextHubHandle + " in loadNanoApp"); return -1; } - if (app == null) { - Log.e(TAG, "Invalid null app"); + if (nanoApp == null) { + Log.e(TAG, "NanoApp cannot be null in loadNanoApp"); return -1; } // Create an internal IContextHubTransactionCallback for the old API clients - NanoAppBinary nanoAppBinary = new NanoAppBinary(app.getAppBinary()); + NanoAppBinary nanoAppBinary = new NanoAppBinary(nanoApp.getAppBinary()); IContextHubTransactionCallback onCompleteCallback = - createLoadTransactionCallback(contextHubId, nanoAppBinary); + createLoadTransactionCallback(contextHubHandle, nanoAppBinary); ContextHubServiceTransaction transaction = mTransactionManager.createLoadTransaction( - contextHubId, nanoAppBinary, onCompleteCallback); + contextHubHandle, nanoAppBinary, onCompleteCallback); mTransactionManager.addTransaction(transaction); return 0; @@ -384,7 +381,7 @@ public class ContextHubService extends IContextHubService.Stub { NanoAppInstanceInfo info = mNanoAppStateManager.getNanoAppInstanceInfo(nanoAppHandle); if (info == null) { - Log.e(TAG, "Cannot find nanoapp with handle " + nanoAppHandle); + Log.e(TAG, "Invalid nanoapp handle " + nanoAppHandle + " in unloadNanoApp"); return -1; } @@ -407,7 +404,8 @@ public class ContextHubService extends IContextHubService.Stub { } @Override - public int[] findNanoAppOnHub(int hubHandle, NanoAppFilter filter) throws RemoteException { + public int[] findNanoAppOnHub( + int contextHubHandle, NanoAppFilter filter) throws RemoteException { checkPermissions(); ArrayList<Integer> foundInstances = new ArrayList<>(); @@ -450,29 +448,29 @@ public class ContextHubService extends IContextHubService.Stub { } @Override - public int sendMessage( - int hubHandle, int nanoAppHandle, ContextHubMessage msg) throws RemoteException { + public int sendMessage(int contextHubHandle, int nanoAppHandle, ContextHubMessage msg) + throws RemoteException { checkPermissions(); if (mContextHubProxy == null) { return -1; } if (msg == null) { - Log.e(TAG, "ContextHubMessage cannot be null"); + Log.e(TAG, "ContextHubMessage cannot be null in sendMessage"); return -1; } if (msg.getData() == null) { - Log.e(TAG, "ContextHubMessage message body cannot be null"); + Log.e(TAG, "ContextHubMessage message body cannot be null in sendMessage"); return -1; } - if (!mDefaultClientMap.containsKey(hubHandle)) { - Log.e(TAG, "Hub with ID " + hubHandle + " does not exist"); + if (!isValidContextHubId(contextHubHandle)) { + Log.e(TAG, "Invalid Context Hub handle " + contextHubHandle + " in sendMessage"); return -1; } boolean success = false; if (nanoAppHandle == OS_APP_INSTANCE) { if (msg.getMsgType() == MSG_QUERY_NANO_APPS) { - success = (queryNanoAppsInternal(hubHandle) == Result.OK); + success = (queryNanoAppsInternal(contextHubHandle) == Result.OK); } else { Log.e(TAG, "Invalid OS message params of type " + msg.getMsgType()); } @@ -482,7 +480,7 @@ public class ContextHubService extends IContextHubService.Stub { NanoAppMessage message = NanoAppMessage.createMessageToNanoApp( info.getAppId(), msg.getMsgType(), msg.getData()); - IContextHubClient client = mDefaultClientMap.get(hubHandle); + IContextHubClient client = mDefaultClientMap.get(contextHubHandle); success = (client.sendMessageToNanoApp(message) == ContextHubTransaction.TRANSACTION_SUCCESS); } else { @@ -595,13 +593,7 @@ public class ContextHubService extends IContextHubService.Stub { * @return {@code true} if the ID represents that of an available hub, {@code false} otherwise */ private boolean isValidContextHubId(int contextHubId) { - for (ContextHubInfo hubInfo : mContextHubInfo) { - if (hubInfo.getId() == contextHubId) { - return true; - } - } - - return false; + return mContextHubIdToInfoMap.containsKey(contextHubId); } /** @@ -762,8 +754,8 @@ public class ContextHubService extends IContextHubService.Stub { pw.println(""); // dump ContextHubInfo pw.println("=================== CONTEXT HUBS ===================="); - for (int i = 0; i < mContextHubInfo.length; i++) { - pw.println("Handle " + i + " : " + mContextHubInfo[i].toString()); + for (ContextHubInfo hubInfo : mContextHubIdToInfoMap.values()) { + pw.println(hubInfo); } pw.println(""); pw.println("=================== NANOAPPS ===================="); @@ -779,7 +771,8 @@ public class ContextHubService extends IContextHubService.Stub { ContextHubServiceUtil.checkPermissions(mContext); } - private int onMessageReceiptOldApi(int msgType, int hubHandle, int appInstance, byte[] data) { + private int onMessageReceiptOldApi( + int msgType, int contextHubHandle, int appInstance, byte[] data) { if (data == null) { return -1; } @@ -787,7 +780,8 @@ public class ContextHubService extends IContextHubService.Stub { int msgVersion = 0; int callbacksCount = mCallbacksList.beginBroadcast(); Log.d(TAG, "Sending message " + msgType + " version " + msgVersion + " from hubHandle " + - hubHandle + ", appInstance " + appInstance + ", callBackCount " + callbacksCount); + contextHubHandle + ", appInstance " + appInstance + ", callBackCount " + + callbacksCount); if (callbacksCount < 1) { Log.v(TAG, "No message callbacks registered."); @@ -798,7 +792,7 @@ public class ContextHubService extends IContextHubService.Stub { for (int i = 0; i < callbacksCount; ++i) { IContextHubCallback callback = mCallbacksList.getBroadcastItem(i); try { - callback.onMessageReceipt(hubHandle, appInstance, msg); + callback.onMessageReceipt(contextHubHandle, appInstance, msg); } catch (RemoteException e) { Log.i(TAG, "Exception (" + e + ") calling remote callback (" + callback + ")."); continue; diff --git a/services/core/java/com/android/server/location/ContextHubServiceUtil.java b/services/core/java/com/android/server/location/ContextHubServiceUtil.java index 6faeb72e2a4d..7a57dd38c95f 100644 --- a/services/core/java/com/android/server/location/ContextHubServiceUtil.java +++ b/services/core/java/com/android/server/location/ContextHubServiceUtil.java @@ -30,6 +30,9 @@ import android.hardware.location.NanoAppMessage; import android.hardware.location.NanoAppState; import android.util.Log; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.ArrayList; @@ -43,19 +46,20 @@ import java.util.ArrayList; + HARDWARE_PERMISSION + "' not granted to access ContextHub Hardware"; /** - * Creates a ContextHubInfo array from an ArrayList of HIDL ContextHub objects. + * Creates a ConcurrentHashMap of the Context Hub ID to the ContextHubInfo object given an + * ArrayList of HIDL ContextHub objects. * * @param hubList the ContextHub ArrayList - * @return the ContextHubInfo array + * @return the HashMap object */ /* package */ - static ContextHubInfo[] createContextHubInfoArray(List<ContextHub> hubList) { - ContextHubInfo[] contextHubInfoList = new ContextHubInfo[hubList.size()]; - for (int i = 0; i < hubList.size(); i++) { - contextHubInfoList[i] = new ContextHubInfo(hubList.get(i)); + static HashMap<Integer, ContextHubInfo> createContextHubInfoMap(List<ContextHub> hubList) { + HashMap<Integer, ContextHubInfo> contextHubIdToInfoMap = new HashMap<>(); + for (ContextHub contextHub : hubList) { + contextHubIdToInfoMap.put(contextHub.hubId, new ContextHubInfo(contextHub)); } - return contextHubInfoList; + return contextHubIdToInfoMap; } /** @@ -90,6 +94,22 @@ import java.util.ArrayList; } /** + * Creates a primitive integer array given a Collection<Integer>. + * @param collection the collection to iterate + * @return the primitive integer array + */ + static int[] createPrimitiveIntArray(Collection<Integer> collection) { + int[] primitiveArray = new int[collection.size()]; + + int i = 0; + for (int contextHubId : collection) { + primitiveArray[i++] = contextHubId; + } + + return primitiveArray; + } + + /** * Generates the Context Hub HAL's NanoAppBinary object from the client-facing * android.hardware.location.NanoAppBinary object. * diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java index 742cb4591864..d8a2d31f6703 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java @@ -17,23 +17,159 @@ package com.android.server.locksettings.recoverablekeystore; import android.annotation.Nullable; - +import com.android.internal.annotations.VisibleForTesting; +import java.math.BigInteger; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; - +import java.security.SecureRandom; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECGenParameterSpec; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; import javax.crypto.AEADBadTagException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyAgreement; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; /** - * TODO(b/69056040) Add implementation of SecureBox. This is a placeholder so KeySyncUtils compiles. + * Implementation of the SecureBox v2 crypto functions. + * + * <p>Securebox v2 provides a simple interface to perform encryptions by using any of the following + * credential types: + * + * <ul> + * <li>A public key owned by the recipient, + * <li>A secret shared between the sender and the recipient, or + * <li>Both a recipient's public key and a shared secret. + * </ul> * * @hide */ public class SecureBox { + + private static final byte[] VERSION = new byte[] {(byte) 0x02, 0}; // LITTLE_ENDIAN_TWO_BYTES(2) + private static final byte[] HKDF_SALT = + concat("SECUREBOX".getBytes(StandardCharsets.UTF_8), VERSION); + private static final byte[] HKDF_INFO_WITH_PUBLIC_KEY = + "P256 HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8); + private static final byte[] HKDF_INFO_WITHOUT_PUBLIC_KEY = + "SHARED HKDF-SHA-256 AES-128-GCM".getBytes(StandardCharsets.UTF_8); + private static final byte[] CONSTANT_01 = {(byte) 0x01}; + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + private static final byte EC_PUBLIC_KEY_PREFIX = (byte) 0x04; + + private static final String CIPHER_ALG = "AES"; + private static final String EC_ALG = "EC"; + private static final String EC_P256_COMMON_NAME = "secp256r1"; + private static final String EC_P256_OPENSSL_NAME = "prime256v1"; + private static final String ENC_ALG = "AES/GCM/NoPadding"; + private static final String KA_ALG = "ECDH"; + private static final String MAC_ALG = "HmacSHA256"; + + private static final int EC_COORDINATE_LEN_BYTES = 32; + private static final int EC_PUBLIC_KEY_LEN_BYTES = 2 * EC_COORDINATE_LEN_BYTES + 1; + private static final int GCM_NONCE_LEN_BYTES = 12; + private static final int GCM_KEY_LEN_BYTES = 16; + private static final int GCM_TAG_LEN_BYTES = 16; + + private static final BigInteger BIG_INT_02 = BigInteger.valueOf(2); + + private enum AesGcmOperation { + ENCRYPT, + DECRYPT + } + + // Parameters for the NIST P-256 curve y^2 = x^3 + ax + b (mod p) + private static final BigInteger EC_PARAM_P = + new BigInteger("ffffffff00000001000000000000000000000000ffffffffffffffffffffffff", 16); + private static final BigInteger EC_PARAM_A = EC_PARAM_P.subtract(new BigInteger("3")); + private static final BigInteger EC_PARAM_B = + new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16); + + @VisibleForTesting static final ECParameterSpec EC_PARAM_SPEC; + + static { + EllipticCurve curveSpec = + new EllipticCurve(new ECFieldFp(EC_PARAM_P), EC_PARAM_A, EC_PARAM_B); + ECPoint generator = + new ECPoint( + new BigInteger( + "6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", + 16), + new BigInteger( + "4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", + 16)); + BigInteger generatorOrder = + new BigInteger( + "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16); + EC_PARAM_SPEC = new ECParameterSpec(curveSpec, generator, generatorOrder, /* cofactor */ 1); + } + + private SecureBox() {} + /** - * TODO(b/69056040) Add implementation of encrypt. + * Randomly generates a public-key pair that can be used for the functions {@link #encrypt} and + * {@link #decrypt}. * + * @return the randomly generated public-key pair + * @throws NoSuchAlgorithmException if the underlying crypto algorithm is not supported + * @hide + */ + public static KeyPair genKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(EC_ALG); + try { + // Try using the OpenSSL provider first + keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_OPENSSL_NAME)); + return keyPairGenerator.generateKeyPair(); + } catch (InvalidAlgorithmParameterException ex) { + // Try another name for NIST P-256 + } + try { + keyPairGenerator.initialize(new ECGenParameterSpec(EC_P256_COMMON_NAME)); + return keyPairGenerator.generateKeyPair(); + } catch (InvalidAlgorithmParameterException ex) { + throw new NoSuchAlgorithmException("Unable to find the NIST P-256 curve", ex); + } + } + + /** + * Encrypts {@code payload} by using {@code theirPublicKey} and/or {@code sharedSecret}. At + * least one of {@code theirPublicKey} and {@code sharedSecret} must be non-null, and an empty + * {@code sharedSecret} is equivalent to null. + * + * <p>Note that {@code header} will be authenticated (but not encrypted) together with {@code + * payload}, and the same {@code header} has to be provided for {@link #decrypt}. + * + * @param theirPublicKey the recipient's public key, or null if the payload is to be encrypted + * only with the shared secret + * @param sharedSecret the secret shared between the sender and the recipient, or null if the + * payload is to be encrypted only with the recipient's public key + * @param header the data that will be authenticated with {@code payload} but not encrypted, or + * null if the data is empty + * @param payload the data to be encrypted, or null if the data is empty + * @return the encrypted payload + * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported + * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms * @hide */ public static byte[] encrypt( @@ -42,12 +178,59 @@ public class SecureBox { @Nullable byte[] header, @Nullable byte[] payload) throws NoSuchAlgorithmException, InvalidKeyException { - throw new UnsupportedOperationException("Needs to be implemented."); + sharedSecret = emptyByteArrayIfNull(sharedSecret); + if (theirPublicKey == null && sharedSecret.length == 0) { + throw new IllegalArgumentException("Both the public key and shared secret are empty"); + } + header = emptyByteArrayIfNull(header); + payload = emptyByteArrayIfNull(payload); + + KeyPair senderKeyPair; + byte[] dhSecret; + byte[] hkdfInfo; + if (theirPublicKey == null) { + senderKeyPair = null; + dhSecret = EMPTY_BYTE_ARRAY; + hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY; + } else { + senderKeyPair = genKeyPair(); + dhSecret = dhComputeSecret(senderKeyPair.getPrivate(), theirPublicKey); + hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY; + } + + byte[] randNonce = genRandomNonce(); + byte[] keyingMaterial = concat(dhSecret, sharedSecret); + SecretKey encryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo); + byte[] ciphertext = aesGcmEncrypt(encryptionKey, randNonce, payload, header); + if (senderKeyPair == null) { + return concat(VERSION, randNonce, ciphertext); + } else { + return concat( + VERSION, encodePublicKey(senderKeyPair.getPublic()), randNonce, ciphertext); + } } /** - * TODO(b/69056040) Add implementation of decrypt. + * Decrypts {@code encryptedPayload} by using {@code ourPrivateKey} and/or {@code sharedSecret}. + * At least one of {@code ourPrivateKey} and {@code sharedSecret} must be non-null, and an empty + * {@code sharedSecret} is equivalent to null. * + * <p>Note that {@code header} should be the same data used for {@link #encrypt}, which is + * authenticated (but not encrypted) together with {@code payload}; otherwise, an {@code + * AEADBadTagException} will be thrown. + * + * @param ourPrivateKey the recipient's private key, or null if the payload was encrypted only + * with the shared secret + * @param sharedSecret the secret shared between the sender and the recipient, or null if the + * payload was encrypted only with the recipient's public key + * @param header the data that was authenticated with the original payload but not encrypted, or + * null if the data is empty + * @param encryptedPayload the data to be decrypted + * @return the original payload that was encrypted + * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported + * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms + * @throws AEADBadTagException if the authentication tag contained in {@code encryptedPayload} + * cannot be validated * @hide */ public static byte[] decrypt( @@ -56,6 +239,224 @@ public class SecureBox { @Nullable byte[] header, byte[] encryptedPayload) throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { - throw new UnsupportedOperationException("Needs to be implemented."); + sharedSecret = emptyByteArrayIfNull(sharedSecret); + if (ourPrivateKey == null && sharedSecret.length == 0) { + throw new IllegalArgumentException("Both the private key and shared secret are empty"); + } + header = emptyByteArrayIfNull(header); + encryptedPayload = emptyByteArrayIfNull(encryptedPayload); + + ByteBuffer ciphertextBuffer = ByteBuffer.wrap(encryptedPayload); + byte[] version = readEncryptedPayload(ciphertextBuffer, VERSION.length); + if (!Arrays.equals(version, VERSION)) { + throw new IllegalArgumentException("The payload was not encrypted by SecureBox v2"); + } + + byte[] senderPublicKeyBytes; + byte[] dhSecret; + byte[] hkdfInfo; + if (ourPrivateKey == null) { + dhSecret = EMPTY_BYTE_ARRAY; + hkdfInfo = HKDF_INFO_WITHOUT_PUBLIC_KEY; + } else { + senderPublicKeyBytes = readEncryptedPayload(ciphertextBuffer, EC_PUBLIC_KEY_LEN_BYTES); + dhSecret = dhComputeSecret(ourPrivateKey, decodePublicKey(senderPublicKeyBytes)); + hkdfInfo = HKDF_INFO_WITH_PUBLIC_KEY; + } + + byte[] randNonce = readEncryptedPayload(ciphertextBuffer, GCM_NONCE_LEN_BYTES); + byte[] ciphertext = readEncryptedPayload(ciphertextBuffer, ciphertextBuffer.remaining()); + byte[] keyingMaterial = concat(dhSecret, sharedSecret); + SecretKey decryptionKey = hkdfDeriveKey(keyingMaterial, HKDF_SALT, hkdfInfo); + return aesGcmDecrypt(decryptionKey, randNonce, ciphertext, header); + } + + private static byte[] readEncryptedPayload(ByteBuffer buffer, int length) { + byte[] output = new byte[length]; + try { + buffer.get(output); + } catch (BufferUnderflowException ex) { + throw new IllegalArgumentException("The encrypted payload is too short"); + } + return output; + } + + private static byte[] dhComputeSecret(PrivateKey ourPrivateKey, PublicKey theirPublicKey) + throws NoSuchAlgorithmException, InvalidKeyException { + KeyAgreement agreement = KeyAgreement.getInstance(KA_ALG); + try { + agreement.init(ourPrivateKey); + } catch (RuntimeException ex) { + // Rethrow the RuntimeException as InvalidKeyException + throw new InvalidKeyException(ex); + } + agreement.doPhase(theirPublicKey, /*lastPhase=*/ true); + return agreement.generateSecret(); + } + + /** Derives a 128-bit AES key. */ + private static SecretKey hkdfDeriveKey(byte[] secret, byte[] salt, byte[] info) + throws NoSuchAlgorithmException { + Mac mac = Mac.getInstance(MAC_ALG); + try { + mac.init(new SecretKeySpec(salt, MAC_ALG)); + } catch (InvalidKeyException ex) { + // This should never happen + throw new RuntimeException(ex); + } + byte[] pseudorandomKey = mac.doFinal(secret); + + try { + mac.init(new SecretKeySpec(pseudorandomKey, MAC_ALG)); + } catch (InvalidKeyException ex) { + // This should never happen + throw new RuntimeException(ex); + } + mac.update(info); + // Hashing just one block will yield 256 bits, which is enough to construct the AES key + byte[] hkdfOutput = mac.doFinal(CONSTANT_01); + + return new SecretKeySpec(Arrays.copyOf(hkdfOutput, GCM_KEY_LEN_BYTES), CIPHER_ALG); + } + + private static byte[] aesGcmEncrypt(SecretKey key, byte[] nonce, byte[] plaintext, byte[] aad) + throws NoSuchAlgorithmException, InvalidKeyException { + try { + return aesGcmInternal(AesGcmOperation.ENCRYPT, key, nonce, plaintext, aad); + } catch (AEADBadTagException ex) { + // This should never happen + throw new RuntimeException(ex); + } + } + + private static byte[] aesGcmDecrypt(SecretKey key, byte[] nonce, byte[] ciphertext, byte[] aad) + throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { + return aesGcmInternal(AesGcmOperation.DECRYPT, key, nonce, ciphertext, aad); + } + + private static byte[] aesGcmInternal( + AesGcmOperation operation, SecretKey key, byte[] nonce, byte[] text, byte[] aad) + throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException { + Cipher cipher; + try { + cipher = Cipher.getInstance(ENC_ALG); + } catch (NoSuchPaddingException ex) { + // This should never happen because AES-GCM doesn't use padding + throw new RuntimeException(ex); + } + GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LEN_BYTES * 8, nonce); + try { + if (operation == AesGcmOperation.DECRYPT) { + cipher.init(Cipher.DECRYPT_MODE, key, spec); + } else { + cipher.init(Cipher.ENCRYPT_MODE, key, spec); + } + } catch (InvalidAlgorithmParameterException ex) { + // This should never happen + throw new RuntimeException(ex); + } + try { + cipher.updateAAD(aad); + return cipher.doFinal(text); + } catch (AEADBadTagException ex) { + // Catch and rethrow AEADBadTagException first because it's a subclass of + // BadPaddingException + throw ex; + } catch (IllegalBlockSizeException | BadPaddingException ex) { + // This should never happen because AES-GCM can handle inputs of any length without + // padding + throw new RuntimeException(ex); + } + } + + @VisibleForTesting + static byte[] encodePublicKey(PublicKey publicKey) { + ECPoint point = ((ECPublicKey) publicKey).getW(); + byte[] x = point.getAffineX().toByteArray(); + byte[] y = point.getAffineY().toByteArray(); + + byte[] output = new byte[EC_PUBLIC_KEY_LEN_BYTES]; + // The order of arraycopy() is important, because the coordinates may have a one-byte + // leading 0 for the sign bit of two's complement form + System.arraycopy(y, 0, output, EC_PUBLIC_KEY_LEN_BYTES - y.length, y.length); + System.arraycopy(x, 0, output, 1 + EC_COORDINATE_LEN_BYTES - x.length, x.length); + output[0] = EC_PUBLIC_KEY_PREFIX; + return output; + } + + @VisibleForTesting + static PublicKey decodePublicKey(byte[] keyBytes) + throws NoSuchAlgorithmException, InvalidKeyException { + BigInteger x = + new BigInteger( + /*signum=*/ 1, + Arrays.copyOfRange(keyBytes, 1, 1 + EC_COORDINATE_LEN_BYTES)); + BigInteger y = + new BigInteger( + /*signum=*/ 1, + Arrays.copyOfRange( + keyBytes, 1 + EC_COORDINATE_LEN_BYTES, EC_PUBLIC_KEY_LEN_BYTES)); + + // Checks if the point is indeed on the P-256 curve for security considerations + validateEcPoint(x, y); + + KeyFactory keyFactory = KeyFactory.getInstance(EC_ALG); + try { + return keyFactory.generatePublic(new ECPublicKeySpec(new ECPoint(x, y), EC_PARAM_SPEC)); + } catch (InvalidKeySpecException ex) { + // This should never happen + throw new RuntimeException(ex); + } + } + + private static void validateEcPoint(BigInteger x, BigInteger y) throws InvalidKeyException { + if (x.compareTo(EC_PARAM_P) >= 0 + || y.compareTo(EC_PARAM_P) >= 0 + || x.signum() == -1 + || y.signum() == -1) { + throw new InvalidKeyException("Point lies outside of the expected curve"); + } + + // Points on the curve satisfy y^2 = x^3 + ax + b (mod p) + BigInteger lhs = y.modPow(BIG_INT_02, EC_PARAM_P); + BigInteger rhs = + x.modPow(BIG_INT_02, EC_PARAM_P) // x^2 + .add(EC_PARAM_A) // x^2 + a + .mod(EC_PARAM_P) // This will speed up the next multiplication + .multiply(x) // (x^2 + a) * x = x^3 + ax + .add(EC_PARAM_B) // x^3 + ax + b + .mod(EC_PARAM_P); + if (!lhs.equals(rhs)) { + throw new InvalidKeyException("Point lies outside of the expected curve"); + } + } + + private static byte[] genRandomNonce() throws NoSuchAlgorithmException { + byte[] nonce = new byte[GCM_NONCE_LEN_BYTES]; + new SecureRandom().nextBytes(nonce); + return nonce; + } + + @VisibleForTesting + static byte[] concat(byte[]... inputs) { + int length = 0; + for (int i = 0; i < inputs.length; i++) { + if (inputs[i] == null) { + inputs[i] = EMPTY_BYTE_ARRAY; + } + length += inputs[i].length; + } + + byte[] output = new byte[length]; + int outputPos = 0; + for (byte[] input : inputs) { + System.arraycopy(input, /*srcPos=*/ 0, output, outputPos, input.length); + outputPos += input.length; + } + return output; + } + + private static byte[] emptyByteArrayIfNull(@Nullable byte[] input) { + return input == null ? EMPTY_BYTE_ARRAY : input; } } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java index 79865337d4c1..3644d368a7f3 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java @@ -25,6 +25,7 @@ import android.util.Log; import com.android.server.locksettings.recoverablekeystore.WrappedKey; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry; +import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.UserMetadataEntry; import java.util.HashMap; import java.util.Locale; @@ -176,6 +177,52 @@ public class RecoverableKeyStoreDb { } /** + * Sets the {@code generationId} of the platform key for the account owned by {@code userId}. + * + * @return The primary key ID of the relation. + */ + public long setPlatformKeyGenerationId(int userId, int generationId) { + SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(UserMetadataEntry.COLUMN_NAME_USER_ID, userId); + values.put(UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID, generationId); + return db.replace( + UserMetadataEntry.TABLE_NAME, /*nullColumnHack=*/ null, values); + } + + /** + * Returns the generation ID associated with the platform key of the user with {@code userId}. + */ + public int getPlatformKeyGenerationId(int userId) { + SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase(); + String[] projection = { + UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID}; + String selection = + UserMetadataEntry.COLUMN_NAME_USER_ID + " = ?"; + String[] selectionArguments = { + Integer.toString(userId)}; + + try ( + Cursor cursor = db.query( + UserMetadataEntry.TABLE_NAME, + projection, + selection, + selectionArguments, + /*groupBy=*/ null, + /*having=*/ null, + /*orderBy=*/ null) + ) { + if (cursor.getCount() == 0) { + return -1; + } + cursor.moveToFirst(); + return cursor.getInt( + cursor.getColumnIndexOrThrow( + UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID)); + } + } + + /** * Closes all open connections to the database. */ public void close() { diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java index c54d0a6f4f77..b6c168f41078 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java @@ -58,4 +58,22 @@ class RecoverableKeyStoreDbContract { */ static final String COLUMN_NAME_LAST_SYNCED_AT = "last_synced_at"; } + + /** + * Recoverable KeyStore metadata for a specific user profile. + */ + static class UserMetadataEntry implements BaseColumns { + static final String TABLE_NAME = "user_metadata"; + + /** + * User ID of the profile. + */ + static final String COLUMN_NAME_USER_ID = "user_id"; + + /** + * Every time a new platform key is generated for a user, this increments. The platform key + * is used to wrap recoverable keys on disk. + */ + static final String COLUMN_NAME_PLATFORM_KEY_GENERATION_ID = "platform_key_generation_id"; + } } diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java index e3783c496ee2..686820323241 100644 --- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java @@ -5,6 +5,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry; +import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.UserMetadataEntry; /** * Helper for creating the recoverable key database. @@ -13,31 +14,44 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper { private static final int DATABASE_VERSION = 1; private static final String DATABASE_NAME = "recoverablekeystore.db"; - private static final String SQL_CREATE_ENTRIES = + private static final String SQL_CREATE_KEYS_ENTRY = "CREATE TABLE " + KeysEntry.TABLE_NAME + "( " + KeysEntry._ID + " INTEGER PRIMARY KEY," - + KeysEntry.COLUMN_NAME_UID + " INTEGER UNIQUE," - + KeysEntry.COLUMN_NAME_ALIAS + " TEXT UNIQUE," + + KeysEntry.COLUMN_NAME_UID + " INTEGER," + + KeysEntry.COLUMN_NAME_ALIAS + " TEXT," + KeysEntry.COLUMN_NAME_NONCE + " BLOB," + KeysEntry.COLUMN_NAME_WRAPPED_KEY + " BLOB," + KeysEntry.COLUMN_NAME_GENERATION_ID + " INTEGER," - + KeysEntry.COLUMN_NAME_LAST_SYNCED_AT + " INTEGER)"; + + KeysEntry.COLUMN_NAME_LAST_SYNCED_AT + " INTEGER," + + "UNIQUE(" + KeysEntry.COLUMN_NAME_UID + "," + + KeysEntry.COLUMN_NAME_ALIAS + "))"; - private static final String SQL_DELETE_ENTRIES = + private static final String SQL_CREATE_USER_METADATA_ENTRY = + "CREATE TABLE " + UserMetadataEntry.TABLE_NAME + "( " + + UserMetadataEntry._ID + " INTEGER PRIMARY KEY," + + UserMetadataEntry.COLUMN_NAME_USER_ID + " INTEGER UNIQUE," + + UserMetadataEntry.COLUMN_NAME_PLATFORM_KEY_GENERATION_ID + " INTEGER)"; + + private static final String SQL_DELETE_KEYS_ENTRY = "DROP TABLE IF EXISTS " + KeysEntry.TABLE_NAME; + private static final String SQL_DELETE_USER_METADATA_ENTRY = + "DROP TABLE IF EXISTS " + UserMetadataEntry.TABLE_NAME; + RecoverableKeyStoreDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { - db.execSQL(SQL_CREATE_ENTRIES); + db.execSQL(SQL_CREATE_KEYS_ENTRY); + db.execSQL(SQL_CREATE_USER_METADATA_ENTRY); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - db.execSQL(SQL_DELETE_ENTRIES); + db.execSQL(SQL_DELETE_KEYS_ENTRY); + db.execSQL(SQL_DELETE_USER_METADATA_ENTRY); onCreate(db); } } diff --git a/services/core/java/com/android/server/vr/VrManagerService.java b/services/core/java/com/android/server/vr/VrManagerService.java index 21c688907dc1..de723c6701d3 100644 --- a/services/core/java/com/android/server/vr/VrManagerService.java +++ b/services/core/java/com/android/server/vr/VrManagerService.java @@ -59,6 +59,8 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; + +import com.android.server.FgThread; import com.android.server.wm.WindowManagerInternal; import android.view.inputmethod.InputMethodManagerInternal; @@ -825,9 +827,11 @@ public class VrManagerService extends SystemService @Override public void onSwitchUser(int userHandle) { - synchronized (mLock) { - mComponentObserver.onUsersChanged(); - } + FgThread.getHandler().post(() -> { + synchronized (mLock) { + mComponentObserver.onUsersChanged(); + } + }); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java index da42dc936132..e55d4ea35739 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java @@ -91,4 +91,9 @@ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub { public boolean isUsingUnifiedPassword(ComponentName who) { return true; } + + public boolean setKeyPairCertificate(ComponentName who, String callerPackage, String alias, + byte[] cert, byte[] chain, boolean isUserSelectable) { + return false; + } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index d1a93d06b624..387818be6480 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -5154,6 +5154,33 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @Override + public boolean setKeyPairCertificate(ComponentName who, String callerPackage, String alias, + byte[] cert, byte[] chain, boolean isUserSelectable) { + enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, + DELEGATION_CERT_INSTALL); + + final int callingUid = mInjector.binderGetCallingUid(); + final long id = mInjector.binderClearCallingIdentity(); + try (final KeyChainConnection keyChainConnection = + KeyChain.bindAsUser(mContext, UserHandle.getUserHandleForUid(callingUid))) { + IKeyChainService keyChain = keyChainConnection.getService(); + if (!keyChain.setKeyPairCertificate(alias, cert, chain)) { + return false; + } + keyChain.setUserSelectable(alias, isUserSelectable); + return true; + } catch (InterruptedException e) { + Log.w(LOG_TAG, "Interrupted while setting keypair certificate", e); + Thread.currentThread().interrupt(); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Failed setting keypair certificate", e); + } finally { + mInjector.binderRestoreCallingIdentity(id); + } + return false; + } + + @Override public void choosePrivateKeyAlias(final int uid, final Uri uri, final String alias, final IBinder response) { // Caller UID needs to be trusted, so we restrict this method to SYSTEM_UID callers. diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/SecureBoxTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/SecureBoxTest.java new file mode 100644 index 000000000000..72b69f046b33 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/SecureBoxTest.java @@ -0,0 +1,365 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings.recoverablekeystore; + +import static com.google.common.truth.Truth.assertThat; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.expectThrows; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.ECPrivateKeySpec; +import javax.crypto.AEADBadTagException; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SecureBoxTest { + + private static final int EC_PUBLIC_KEY_LEN_BYTES = 65; + private static final int NUM_TEST_ITERATIONS = 100; + private static final int VERSION_LEN_BYTES = 2; + + // The following fixtures were produced by the C implementation of SecureBox v2. We use these to + // cross-verify the two implementations. + private static final byte[] VAULT_PARAMS = + new byte[] { + (byte) 0x04, (byte) 0xb8, (byte) 0x00, (byte) 0x11, (byte) 0x18, (byte) 0x98, + (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, (byte) 0xb4, (byte) 0x94, (byte) 0xfe, + (byte) 0x86, (byte) 0xda, (byte) 0x1c, (byte) 0x07, (byte) 0x8d, (byte) 0x01, + (byte) 0xb4, (byte) 0x3a, (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, (byte) 0x61, + (byte) 0xd0, (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10, + (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, (byte) 0x3f, + (byte) 0xd2, (byte) 0xdf, (byte) 0xf3, (byte) 0x79, (byte) 0x20, (byte) 0x1d, + (byte) 0x91, (byte) 0x55, (byte) 0xb0, (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, + (byte) 0x8b, (byte) 0x32, (byte) 0x7d, (byte) 0x25, (byte) 0x53, (byte) 0xa2, + (byte) 0xfc, (byte) 0xa5, (byte) 0x65, (byte) 0xe1, (byte) 0xbd, (byte) 0x21, + (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa, (byte) 0x31, + (byte) 0x32, (byte) 0x33, (byte) 0x34, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x78, (byte) 0x56, (byte) 0x34, (byte) 0x12, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x0a, (byte) 0x00, (byte) 0x00, + (byte) 0x00 + }; + private static final byte[] VAULT_CHALLENGE = getBytes("Not a real vault challenge"); + private static final byte[] THM_KF_HASH = getBytes("12345678901234567890123456789012"); + private static final byte[] ENCRYPTED_RECOVERY_KEY = + new byte[] { + (byte) 0x02, (byte) 0x00, (byte) 0x04, (byte) 0xe3, (byte) 0xa8, (byte) 0xd0, + (byte) 0x32, (byte) 0x3c, (byte) 0xc7, (byte) 0xe5, (byte) 0xe8, (byte) 0xc1, + (byte) 0x73, (byte) 0x4c, (byte) 0x75, (byte) 0x20, (byte) 0x2e, (byte) 0xb7, + (byte) 0xba, (byte) 0xef, (byte) 0x3e, (byte) 0x3e, (byte) 0xa6, (byte) 0x93, + (byte) 0xe9, (byte) 0xde, (byte) 0xa7, (byte) 0x00, (byte) 0x09, (byte) 0xba, + (byte) 0xa8, (byte) 0x9c, (byte) 0xac, (byte) 0x72, (byte) 0xff, (byte) 0xf6, + (byte) 0x84, (byte) 0x16, (byte) 0xb0, (byte) 0xff, (byte) 0x47, (byte) 0x98, + (byte) 0x53, (byte) 0xc4, (byte) 0xa3, (byte) 0x4a, (byte) 0x54, (byte) 0x21, + (byte) 0x8e, (byte) 0x00, (byte) 0x4b, (byte) 0xfa, (byte) 0xce, (byte) 0xe3, + (byte) 0x79, (byte) 0x8e, (byte) 0x20, (byte) 0x7c, (byte) 0x9b, (byte) 0xc4, + (byte) 0x7c, (byte) 0xd5, (byte) 0x33, (byte) 0x70, (byte) 0x96, (byte) 0xdc, + (byte) 0xa0, (byte) 0x1f, (byte) 0x6e, (byte) 0xbb, (byte) 0x5d, (byte) 0x0c, + (byte) 0x64, (byte) 0x5f, (byte) 0xed, (byte) 0xbf, (byte) 0x79, (byte) 0x8a, + (byte) 0x0e, (byte) 0xd6, (byte) 0x4b, (byte) 0x93, (byte) 0xc9, (byte) 0xcd, + (byte) 0x25, (byte) 0x06, (byte) 0x73, (byte) 0x5e, (byte) 0xdb, (byte) 0xac, + (byte) 0xa8, (byte) 0xeb, (byte) 0x6e, (byte) 0x26, (byte) 0x77, (byte) 0x56, + (byte) 0xd1, (byte) 0x23, (byte) 0x48, (byte) 0xb6, (byte) 0x6a, (byte) 0x15, + (byte) 0xd4, (byte) 0x3e, (byte) 0x38, (byte) 0x7d, (byte) 0x6f, (byte) 0x6f, + (byte) 0x7c, (byte) 0x0b, (byte) 0x93, (byte) 0x4e, (byte) 0xb3, (byte) 0x21, + (byte) 0x44, (byte) 0x86, (byte) 0xf3, (byte) 0x2e + }; + private static final byte[] KEY_CLAIMANT = getBytes("asdfasdfasdfasdf"); + private static final byte[] RECOVERY_CLAIM = + new byte[] { + (byte) 0x02, (byte) 0x00, (byte) 0x04, (byte) 0x16, (byte) 0x75, (byte) 0x5b, + (byte) 0xa2, (byte) 0xdc, (byte) 0x2b, (byte) 0x58, (byte) 0xb9, (byte) 0x66, + (byte) 0xcb, (byte) 0x6f, (byte) 0xb1, (byte) 0xc1, (byte) 0xb0, (byte) 0x1d, + (byte) 0x82, (byte) 0x29, (byte) 0x97, (byte) 0xec, (byte) 0x65, (byte) 0x5e, + (byte) 0xef, (byte) 0x14, (byte) 0xc7, (byte) 0xf0, (byte) 0xf1, (byte) 0x83, + (byte) 0x15, (byte) 0x0b, (byte) 0xcb, (byte) 0x33, (byte) 0x2d, (byte) 0x05, + (byte) 0x20, (byte) 0xdc, (byte) 0xc7, (byte) 0x0d, (byte) 0xc8, (byte) 0xc0, + (byte) 0xc9, (byte) 0xa8, (byte) 0x67, (byte) 0xc8, (byte) 0x16, (byte) 0xfe, + (byte) 0xfb, (byte) 0xb0, (byte) 0x28, (byte) 0x8e, (byte) 0x4f, (byte) 0xd5, + (byte) 0x31, (byte) 0xa7, (byte) 0x94, (byte) 0x33, (byte) 0x23, (byte) 0x15, + (byte) 0x04, (byte) 0xbf, (byte) 0x13, (byte) 0x6a, (byte) 0x28, (byte) 0x8f, + (byte) 0xa6, (byte) 0xfc, (byte) 0x01, (byte) 0xd5, (byte) 0x69, (byte) 0x3d, + (byte) 0x96, (byte) 0x0c, (byte) 0x37, (byte) 0xb4, (byte) 0x1e, (byte) 0x13, + (byte) 0x40, (byte) 0xcc, (byte) 0x44, (byte) 0x19, (byte) 0xf2, (byte) 0xdb, + (byte) 0x49, (byte) 0x80, (byte) 0x9f, (byte) 0xef, (byte) 0xee, (byte) 0x41, + (byte) 0xe6, (byte) 0x3f, (byte) 0xa8, (byte) 0xea, (byte) 0x89, (byte) 0xfe, + (byte) 0x56, (byte) 0x20, (byte) 0xba, (byte) 0x90, (byte) 0x9a, (byte) 0xba, + (byte) 0x0e, (byte) 0x30, (byte) 0xa7, (byte) 0x2b, (byte) 0x0a, (byte) 0x12, + (byte) 0x0b, (byte) 0x03, (byte) 0xd1, (byte) 0x0c, (byte) 0x8e, (byte) 0x82, + (byte) 0x03, (byte) 0xa1, (byte) 0x7f, (byte) 0xc8, (byte) 0xd0, (byte) 0xa9, + (byte) 0x86, (byte) 0x55, (byte) 0x63, (byte) 0xdc, (byte) 0x70, (byte) 0x34, + (byte) 0x21, (byte) 0x2a, (byte) 0x41, (byte) 0x3f, (byte) 0xbb, (byte) 0x82, + (byte) 0x82, (byte) 0xf9, (byte) 0x2b, (byte) 0xd2, (byte) 0x33, (byte) 0x03, + (byte) 0x50, (byte) 0xd2, (byte) 0x27, (byte) 0xeb, (byte) 0x1a + }; + + private static final byte[] TEST_SHARED_SECRET = getBytes("TEST_SHARED_SECRET"); + private static final byte[] TEST_HEADER = getBytes("TEST_HEADER"); + private static final byte[] TEST_PAYLOAD = getBytes("TEST_PAYLOAD"); + + private static final PublicKey THM_PUBLIC_KEY; + private static final PrivateKey THM_PRIVATE_KEY; + + static { + try { + THM_PUBLIC_KEY = + SecureBox.decodePublicKey( + new byte[] { + (byte) 0x04, (byte) 0xb8, (byte) 0x00, (byte) 0x11, (byte) 0x18, + (byte) 0x98, (byte) 0x1d, (byte) 0xf0, (byte) 0x6e, (byte) 0xb4, + (byte) 0x94, (byte) 0xfe, (byte) 0x86, (byte) 0xda, (byte) 0x1c, + (byte) 0x07, (byte) 0x8d, (byte) 0x01, (byte) 0xb4, (byte) 0x3a, + (byte) 0xf6, (byte) 0x8d, (byte) 0xdc, (byte) 0x61, (byte) 0xd0, + (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10, + (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, + (byte) 0x3f, (byte) 0xd2, (byte) 0xdf, (byte) 0xf3, (byte) 0x79, + (byte) 0x20, (byte) 0x1d, (byte) 0x91, (byte) 0x55, (byte) 0xb0, + (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, (byte) 0x8b, (byte) 0x32, + (byte) 0x7d, (byte) 0x25, (byte) 0x53, (byte) 0xa2, (byte) 0xfc, + (byte) 0xa5, (byte) 0x65, (byte) 0xe1, (byte) 0xbd, (byte) 0x21, + (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa + }); + THM_PRIVATE_KEY = + decodePrivateKey( + new byte[] { + (byte) 0x70, (byte) 0x01, (byte) 0xc7, (byte) 0x87, (byte) 0x32, + (byte) 0x2f, (byte) 0x1c, (byte) 0x9a, (byte) 0x6e, (byte) 0xb1, + (byte) 0x91, (byte) 0xca, (byte) 0x4e, (byte) 0xb5, (byte) 0x44, + (byte) 0xba, (byte) 0xc8, (byte) 0x68, (byte) 0xc6, (byte) 0x0a, + (byte) 0x76, (byte) 0xcb, (byte) 0xd3, (byte) 0x63, (byte) 0x67, + (byte) 0x7c, (byte) 0xb0, (byte) 0x11, (byte) 0x82, (byte) 0x65, + (byte) 0x77, (byte) 0x01 + }); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @Test + public void genKeyPair_alwaysReturnsANewKeyPair() throws Exception { + KeyPair keyPair1 = SecureBox.genKeyPair(); + KeyPair keyPair2 = SecureBox.genKeyPair(); + assertThat(keyPair1).isNotEqualTo(keyPair2); + } + + @Test + public void decryptRecoveryClaim() throws Exception { + byte[] claimContent = + SecureBox.decrypt( + THM_PRIVATE_KEY, + /*sharedSecret=*/ null, + SecureBox.concat(getBytes("V1 KF_claim"), VAULT_PARAMS, VAULT_CHALLENGE), + RECOVERY_CLAIM); + assertThat(claimContent).isEqualTo(SecureBox.concat(THM_KF_HASH, KEY_CLAIMANT)); + } + + @Test + public void decryptRecoveryKey_doesNotThrowForValidAuthenticationTag() throws Exception { + SecureBox.decrypt( + THM_PRIVATE_KEY, + THM_KF_HASH, + SecureBox.concat(getBytes("V1 THM_encrypted_recovery_key"), VAULT_PARAMS), + ENCRYPTED_RECOVERY_KEY); + } + + @Test + public void encryptThenDecrypt() throws Exception { + byte[] state = TEST_PAYLOAD; + // Iterate multiple times to amplify any errors + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + state = SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, state); + } + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + state = SecureBox.decrypt(THM_PRIVATE_KEY, TEST_SHARED_SECRET, TEST_HEADER, state); + } + assertThat(state).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullPublicPrivateKeys() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + /*theirPublicKey=*/ null, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + byte[] decrypted = + SecureBox.decrypt( + /*ourPrivateKey=*/ null, TEST_SHARED_SECRET, TEST_HEADER, encrypted); + assertThat(decrypted).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullSharedSecret() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + THM_PUBLIC_KEY, /*sharedSecret=*/ null, TEST_HEADER, TEST_PAYLOAD); + byte[] decrypted = + SecureBox.decrypt(THM_PRIVATE_KEY, /*sharedSecret=*/ null, TEST_HEADER, encrypted); + assertThat(decrypted).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullHeader() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + THM_PUBLIC_KEY, TEST_SHARED_SECRET, /*header=*/ null, TEST_PAYLOAD); + byte[] decrypted = + SecureBox.decrypt(THM_PRIVATE_KEY, TEST_SHARED_SECRET, /*header=*/ null, encrypted); + assertThat(decrypted).isEqualTo(TEST_PAYLOAD); + } + + @Test + public void encryptThenDecrypt_nullPayload() throws Exception { + byte[] encrypted = + SecureBox.encrypt( + THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, /*payload=*/ null); + byte[] decrypted = + SecureBox.decrypt( + THM_PRIVATE_KEY, + TEST_SHARED_SECRET, + TEST_HEADER, + /*encryptedPayload=*/ encrypted); + assertThat(decrypted.length).isEqualTo(0); + } + + @Test + public void encrypt_nullPublicKeyAndSharedSecret() throws Exception { + IllegalArgumentException expected = + expectThrows( + IllegalArgumentException.class, + () -> + SecureBox.encrypt( + /*theirPublicKey=*/ null, + /*sharedSecret=*/ null, + TEST_HEADER, + TEST_PAYLOAD)); + assertThat(expected.getMessage()).contains("public key and shared secret"); + } + + @Test + public void decrypt_nullPrivateKeyAndSharedSecret() throws Exception { + IllegalArgumentException expected = + expectThrows( + IllegalArgumentException.class, + () -> + SecureBox.decrypt( + /*ourPrivateKey=*/ null, + /*sharedSecret=*/ null, + TEST_HEADER, + TEST_PAYLOAD)); + assertThat(expected.getMessage()).contains("private key and shared secret"); + } + + @Test + public void decrypt_nullEncryptedPayload() throws Exception { + IllegalArgumentException expected = + expectThrows( + IllegalArgumentException.class, + () -> + SecureBox.decrypt( + THM_PRIVATE_KEY, + TEST_SHARED_SECRET, + TEST_HEADER, + /*encryptedPayload=*/ null)); + assertThat(expected.getMessage()).contains("payload"); + } + + @Test + public void decrypt_badAuthenticationTag() throws Exception { + byte[] encrypted = + SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + encrypted[encrypted.length - 1] ^= (byte) 1; + + assertThrows( + AEADBadTagException.class, + () -> + SecureBox.decrypt( + THM_PRIVATE_KEY, TEST_SHARED_SECRET, TEST_HEADER, encrypted)); + } + + @Test + public void encrypt_invalidPublicKey() throws Exception { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + PublicKey publicKey = keyGen.genKeyPair().getPublic(); + + assertThrows( + InvalidKeyException.class, + () -> SecureBox.encrypt(publicKey, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD)); + } + + @Test + public void decrypt_invalidPrivateKey() throws Exception { + byte[] encrypted = + SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + PrivateKey privateKey = keyGen.genKeyPair().getPrivate(); + + assertThrows( + InvalidKeyException.class, + () -> SecureBox.decrypt(privateKey, TEST_SHARED_SECRET, TEST_HEADER, encrypted)); + } + + @Test + public void decrypt_publicKeyOutsideCurve() throws Exception { + byte[] encrypted = + SecureBox.encrypt(THM_PUBLIC_KEY, TEST_SHARED_SECRET, TEST_HEADER, TEST_PAYLOAD); + // Flip the least significant bit of the encoded public key + encrypted[VERSION_LEN_BYTES + EC_PUBLIC_KEY_LEN_BYTES - 1] ^= (byte) 1; + + InvalidKeyException expected = + expectThrows( + InvalidKeyException.class, + () -> + SecureBox.decrypt( + THM_PRIVATE_KEY, + TEST_SHARED_SECRET, + TEST_HEADER, + encrypted)); + assertThat(expected.getMessage()).contains("expected curve"); + } + + @Test + public void encodeThenDecodePublicKey() throws Exception { + for (int i = 0; i < NUM_TEST_ITERATIONS; i++) { + PublicKey originalKey = SecureBox.genKeyPair().getPublic(); + byte[] encodedKey = SecureBox.encodePublicKey(originalKey); + PublicKey decodedKey = SecureBox.decodePublicKey(encodedKey); + assertThat(originalKey).isEqualTo(decodedKey); + } + } + + private static byte[] getBytes(String str) { + return str.getBytes(StandardCharsets.UTF_8); + } + + private static PrivateKey decodePrivateKey(byte[] keyBytes) throws Exception { + assertThat(keyBytes.length).isEqualTo(32); + BigInteger priv = new BigInteger(/*signum=*/ 1, keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(new ECPrivateKeySpec(priv, SecureBox.EC_PARAM_SPEC)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java index 3d5b958170a9..d5ad9597bf1c 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java @@ -30,7 +30,6 @@ import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; - import com.android.server.locksettings.recoverablekeystore.WrappedKey; import java.io.File; @@ -65,7 +64,7 @@ public class RecoverableKeyStoreDbTest { WrappedKey oldWrappedKey = new WrappedKey( getUtf8Bytes("nonce1"), getUtf8Bytes("keymaterial1"), - /*platformKeyGenerationId=*/1); + /*platformKeyGenerationId=*/ 1); mRecoverableKeyStoreDb.insertKey( userId, alias, oldWrappedKey); byte[] nonce = getUtf8Bytes("nonce2"); @@ -83,6 +82,29 @@ public class RecoverableKeyStoreDbTest { } @Test + public void insertKey_allowsTwoUidsToHaveSameAlias() { + String alias = "pcoulton"; + WrappedKey key1 = new WrappedKey( + getUtf8Bytes("nonce1"), + getUtf8Bytes("key1"), + /*platformKeyGenerationId=*/ 1); + WrappedKey key2 = new WrappedKey( + getUtf8Bytes("nonce2"), + getUtf8Bytes("key2"), + /*platformKeyGenerationId=*/ 1); + + mRecoverableKeyStoreDb.insertKey(/*uid=*/ 1, alias, key1); + mRecoverableKeyStoreDb.insertKey(/*uid=*/ 2, alias, key2); + + assertArrayEquals( + getUtf8Bytes("nonce1"), + mRecoverableKeyStoreDb.getKey(1, alias).getNonce()); + assertArrayEquals( + getUtf8Bytes("nonce2"), + mRecoverableKeyStoreDb.getKey(2, alias).getNonce()); + } + + @Test public void getKey_returnsNullIfNoKey() { WrappedKey key = mRecoverableKeyStoreDb.getKey( /*userId=*/ 1, /*alias=*/ "hello"); @@ -157,6 +179,29 @@ public class RecoverableKeyStoreDbTest { assertTrue(keys.isEmpty()); } + @Test + public void getPlatformKeyGenerationId_returnsGenerationId() { + int userId = 42; + int generationId = 24; + mRecoverableKeyStoreDb.setPlatformKeyGenerationId(userId, generationId); + + assertEquals(generationId, mRecoverableKeyStoreDb.getPlatformKeyGenerationId(userId)); + } + + @Test + public void getPlatformKeyGenerationId_returnsMinusOneIfNoEntry() { + assertEquals(-1, mRecoverableKeyStoreDb.getPlatformKeyGenerationId(42)); + } + + @Test + public void setPlatformKeyGenerationId_replacesOldEntry() { + int userId = 42; + mRecoverableKeyStoreDb.setPlatformKeyGenerationId(userId, 1); + mRecoverableKeyStoreDb.setPlatformKeyGenerationId(userId, 2); + + assertEquals(2, mRecoverableKeyStoreDb.getPlatformKeyGenerationId(userId)); + } + private static byte[] getUtf8Bytes(String s) { return s.getBytes(StandardCharsets.UTF_8); } diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java b/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java index 337fd50a88d4..0959df2b78bd 100644 --- a/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java +++ b/services/tests/servicestests/src/com/android/server/wm/WindowFrameTests.java @@ -451,9 +451,9 @@ public class WindowFrameTests extends WindowTestsBase { private DisplayCutout createDisplayCutoutFromRect(int left, int top, int right, int bottom) { return DisplayCutout.fromBoundingPolygon(Arrays.asList( new Point(left, top), - new Point (left, bottom), - new Point (right, bottom), - new Point (left, bottom) + new Point(left, bottom), + new Point(right, bottom), + new Point(right, top) )); } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index c0685f927398..44f55511f940 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -67,6 +67,7 @@ import com.android.internal.content.PackageMonitor; import com.android.internal.os.BackgroundThread; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; +import com.android.server.FgThread; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.UiThread; @@ -389,11 +390,13 @@ public class VoiceInteractionManagerService extends SystemService { } public void switchUser(int userHandle) { - synchronized (this) { - mCurUser = userHandle; - mCurUserUnlocked = false; - switchImplementationIfNeededLocked(false); - } + FgThread.getHandler().post(() -> { + synchronized (this) { + mCurUser = userHandle; + mCurUserUnlocked = false; + switchImplementationIfNeededLocked(false); + } + }); } void switchImplementationIfNeeded(boolean force) { diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 93c0c51d8519..9e77992d5d5e 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -1176,12 +1176,14 @@ public class TelephonyManager { } /** - * Returns the NAI. Return null if NAI is not available. - * + * Returns the Network Access Identifier (NAI). Return null if NAI is not available. + * <p> + * Requires Permission: + * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE} */ - /** {@hide}*/ + @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getNai() { - return getNai(getSlotIndex()); + return getNaiBySubscriberId(getSubId()); } /** @@ -1192,11 +1194,18 @@ public class TelephonyManager { /** {@hide}*/ public String getNai(int slotIndex) { int[] subId = SubscriptionManager.getSubId(slotIndex); + if (subId == null) { + return null; + } + return getNaiBySubscriberId(subId[0]); + } + + private String getNaiBySubscriberId(int subId) { try { IPhoneSubInfo info = getSubscriberInfo(); if (info == null) return null; - String nai = info.getNaiForSubscriber(subId[0], mContext.getOpPackageName()); + String nai = info.getNaiForSubscriber(subId, mContext.getOpPackageName()); if (Log.isLoggable(TAG, Log.VERBOSE)) { Rlog.v(TAG, "Nai = " + nai); } diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java index 2507cfee0b56..973df316d280 100644 --- a/telephony/java/android/telephony/euicc/EuiccManager.java +++ b/telephony/java/android/telephony/euicc/EuiccManager.java @@ -168,12 +168,10 @@ public class EuiccManager { public static final String META_DATA_CARRIER_ICON = "android.telephony.euicc.carriericon"; private final Context mContext; - private final IEuiccController mController; /** @hide */ public EuiccManager(Context context) { mContext = context; - mController = IEuiccController.Stub.asInterface(ServiceManager.getService("econtroller")); } /** @@ -189,7 +187,7 @@ public class EuiccManager { public boolean isEnabled() { // In the future, this may reach out to IEuiccController (if non-null) to check any dynamic // restrictions. - return mController != null; + return getIEuiccController() != null; } /** @@ -206,7 +204,7 @@ public class EuiccManager { return null; } try { - return mController.getEid(); + return getIEuiccController().getEid(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -232,7 +230,7 @@ public class EuiccManager { return; } try { - mController.downloadSubscription(subscription, switchAfterDownload, + getIEuiccController().downloadSubscription(subscription, switchAfterDownload, mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -296,7 +294,7 @@ public class EuiccManager { return; } try { - mController.continueOperation(resolutionIntent, resolutionExtras); + getIEuiccController().continueOperation(resolutionIntent, resolutionExtras); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -328,7 +326,7 @@ public class EuiccManager { return; } try { - mController.getDownloadableSubscriptionMetadata( + getIEuiccController().getDownloadableSubscriptionMetadata( subscription, mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -358,7 +356,7 @@ public class EuiccManager { return; } try { - mController.getDefaultDownloadableSubscriptionList( + getIEuiccController().getDefaultDownloadableSubscriptionList( mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -377,7 +375,7 @@ public class EuiccManager { return null; } try { - return mController.getEuiccInfo(); + return getIEuiccController().getEuiccInfo(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -402,7 +400,7 @@ public class EuiccManager { return; } try { - mController.deleteSubscription( + getIEuiccController().deleteSubscription( subscriptionId, mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -429,7 +427,7 @@ public class EuiccManager { return; } try { - mController.switchToSubscription( + getIEuiccController().switchToSubscription( subscriptionId, mContext.getOpPackageName(), callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -455,7 +453,8 @@ public class EuiccManager { return; } try { - mController.updateSubscriptionNickname(subscriptionId, nickname, callbackIntent); + getIEuiccController().updateSubscriptionNickname( + subscriptionId, nickname, callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -477,7 +476,7 @@ public class EuiccManager { return; } try { - mController.eraseSubscriptions(callbackIntent); + getIEuiccController().eraseSubscriptions(callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -507,7 +506,7 @@ public class EuiccManager { return; } try { - mController.retainSubscriptionsForFactoryReset(callbackIntent); + getIEuiccController().retainSubscriptionsForFactoryReset(callbackIntent); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -520,4 +519,8 @@ public class EuiccManager { // Caller canceled the callback; do nothing. } } + + private static IEuiccController getIEuiccController() { + return IEuiccController.Stub.asInterface(ServiceManager.getService("econtroller")); + } } diff --git a/test-mock/Android.mk b/test-mock/Android.mk index a761a070fa26..7926a77ccb01 100644 --- a/test-mock/Android.mk +++ b/test-mock/Android.mk @@ -16,7 +16,12 @@ LOCAL_PATH:= $(call my-dir) -android_test_mock_source_files := $(call all-java-files-under, src/android/test/mock) +# Includes the main framework source to ensure that doclava has access to the +# visibility information for the base classes of the mock classes. Without it +# otherwise hidden methods could be visible. +android_test_mock_source_files := \ + $(call all-java-files-under, src/android/test/mock) \ + $(call all-java-files-under, ../core/java) \ # For unbundled build we'll use the prebuilt jar from prebuilts/sdk. ifeq (,$(TARGET_BUILD_APPS)$(filter true,$(TARGET_BUILD_PDK))) @@ -67,6 +72,8 @@ LOCAL_SOURCE_FILES_ALL_GENERATED := true LOCAL_ADDITIONAL_DEPENDENCIES := $(android_test_mock_gen_stamp) android_test_mock_gen_stamp := +LOCAL_SDK_VERSION := current + include $(BUILD_STATIC_JAVA_LIBRARY) # Archive a copy of the classes.jar in SDK build. @@ -104,4 +111,91 @@ update-android-test-mock-api: $(ANDROID_TEST_MOCK_OUTPUT_API_FILE) | $(ACP) @echo Copying removed.txt $(hide) $(ACP) $(ANDROID_TEST_MOCK_OUTPUT_REMOVED_API_FILE) $(ANDROID_TEST_MOCK_REMOVED_API_FILE) +# Generate the stub source files for android.test.mock.stubs-system +# ================================================================= +include $(CLEAR_VARS) +LOCAL_SRC_FILES := $(android_test_mock_source_files) + +LOCAL_JAVA_LIBRARIES := core-oj core-libart framework +LOCAL_MODULE_CLASS := JAVA_LIBRARIES +LOCAL_DROIDDOC_SOURCE_PATH := $(LOCAL_PATH)/src/android/test/mock + +ANDROID_TEST_MOCK_SYSTEM_OUTPUT_API_FILE := $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/android.test.mock.stubs-system_intermediates/api.txt +ANDROID_TEST_MOCK_SYSTEM_OUTPUT_REMOVED_API_FILE := $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/android.test.mock.stubs-system_intermediates/removed.txt + +ANDROID_TEST_MOCK_SYSTEM_API_FILE := $(LOCAL_PATH)/api/android-test-mock-system-current.txt +ANDROID_TEST_MOCK_SYSTEM_REMOVED_API_FILE := $(LOCAL_PATH)/api/android-test-mock-system-removed.txt + +LOCAL_DROIDDOC_OPTIONS:= \ + -stubpackages android.test.mock \ + -stubs $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/android.test.mock.stubs-system_intermediates/src \ + -nodocs \ + -showAnnotation android.annotation.SystemApi \ + -api $(ANDROID_TEST_MOCK_SYSTEM_OUTPUT_API_FILE) \ + -removedApi $(ANDROID_TEST_MOCK_SYSTEM_OUTPUT_REMOVED_API_FILE) \ + +LOCAL_UNINSTALLABLE_MODULE := true +LOCAL_MODULE := android-test-mock-system-api-stubs-gen + +include $(BUILD_DROIDDOC) + +# Remember the target that will trigger the code generation. +android_test_mock_system_gen_stamp := $(full_target) + +# Add some additional dependencies +$(ANDROID_TEST_MOCK_SYSTEM_OUTPUT_API_FILE): $(full_target) +$(ANDROID_TEST_MOCK_SYSTEM_OUTPUT_REMOVED_API_FILE): $(full_target) + +# Build the android.test.mock.stubs-system library +# ================================================ +include $(CLEAR_VARS) + +LOCAL_MODULE := android.test.mock.stubs-system + +LOCAL_SOURCE_FILES_ALL_GENERATED := true + +# Make sure to run droiddoc first to generate the stub source files. +LOCAL_ADDITIONAL_DEPENDENCIES := $(android_test_mock_system_gen_stamp) +android_test_mock_system_gen_stamp := + +LOCAL_SDK_VERSION := system_current + +include $(BUILD_STATIC_JAVA_LIBRARY) + +# Archive a copy of the classes.jar in SDK build. +$(call dist-for-goals,sdk win_sdk,$(full_classes_jar):android.test.mock.stubs_system.jar) + +# Check that the android.test.mock.stubs-system library has not changed +# ===================================================================== + +# Check that the API we're building hasn't changed from the not-yet-released +# SDK version. +$(eval $(call check-api, \ + check-android-test-mock-system-api-current, \ + $(ANDROID_TEST_MOCK_SYSTEM_API_FILE), \ + $(ANDROID_TEST_MOCK_SYSTEM_OUTPUT_API_FILE), \ + $(ANDROID_TEST_MOCK_SYSTEM_REMOVED_API_FILE), \ + $(ANDROID_TEST_MOCK_SYSTEM_OUTPUT_REMOVED_API_FILE), \ + -error 2 -error 3 -error 4 -error 5 -error 6 \ + -error 7 -error 8 -error 9 -error 10 -error 11 -error 12 -error 13 -error 14 -error 15 \ + -error 16 -error 17 -error 18 -error 19 -error 20 -error 21 -error 23 -error 24 \ + -error 25 -error 26 -error 27, \ + cat $(LOCAL_PATH)/api/apicheck_msg_android_test_mock-system.txt, \ + check-android-test-mock-system-api, \ + $(call doc-timestamp-for,android-test-mock-system-api-stubs-gen) \ + )) + +.PHONY: check-android-test-mock-system-api +checkapi: check-android-test-mock-system-api + +.PHONY: update-android-test-mock-system-api +update-api: update-android-test-mock-system-api + +update-android-test-mock-system-api: $(ANDROID_TEST_MOCK_SYSTEM_OUTPUT_API_FILE) | $(ACP) + @echo Copying current.txt + $(hide) $(ACP) $(ANDROID_TEST_MOCK_SYSTEM_OUTPUT_API_FILE) $(ANDROID_TEST_MOCK_SYSTEM_API_FILE) + @echo Copying removed.txt + $(hide) $(ACP) $(ANDROID_TEST_MOCK_SYSTEM_OUTPUT_REMOVED_API_FILE) $(ANDROID_TEST_MOCK_SYSTEM_REMOVED_API_FILE) + endif # not TARGET_BUILD_APPS not TARGET_BUILD_PDK=true + diff --git a/test-mock/api/android-test-mock-current.txt b/test-mock/api/android-test-mock-current.txt index 3aa350bd9b77..48a1f80f3379 100644 --- a/test-mock/api/android-test-mock-current.txt +++ b/test-mock/api/android-test-mock-current.txt @@ -10,7 +10,6 @@ package android.test.mock { ctor public MockContentProvider(android.content.Context, java.lang.String, java.lang.String, android.content.pm.PathPermission[]); method public android.content.ContentProviderResult[] applyBatch(java.util.ArrayList<android.content.ContentProviderOperation>); method public int delete(android.net.Uri, java.lang.String, java.lang.String[]); - method public final android.content.IContentProvider getIContentProvider(); method public java.lang.String getType(android.net.Uri); method public android.net.Uri insert(android.net.Uri, android.content.ContentValues); method public boolean onCreate(); @@ -22,37 +21,26 @@ package android.test.mock { public class MockContentResolver extends android.content.ContentResolver { ctor public MockContentResolver(); ctor public MockContentResolver(android.content.Context); - method protected android.content.IContentProvider acquireProvider(android.content.Context, java.lang.String); - method protected android.content.IContentProvider acquireUnstableProvider(android.content.Context, java.lang.String); method public void addProvider(java.lang.String, android.content.ContentProvider); - method public boolean releaseProvider(android.content.IContentProvider); - method public boolean releaseUnstableProvider(android.content.IContentProvider); - method public void unstableProviderDied(android.content.IContentProvider); } public class MockContext extends android.content.Context { ctor public MockContext(); method public boolean bindService(android.content.Intent, android.content.ServiceConnection, int); - method public boolean canLoadUnsafeResources(); method public int checkCallingOrSelfPermission(java.lang.String); method public int checkCallingOrSelfUriPermission(android.net.Uri, int); method public int checkCallingPermission(java.lang.String); method public int checkCallingUriPermission(android.net.Uri, int); method public int checkPermission(java.lang.String, int, int); - method public int checkPermission(java.lang.String, int, int, android.os.IBinder); method public int checkSelfPermission(java.lang.String); method public int checkUriPermission(android.net.Uri, int, int, int); - method public int checkUriPermission(android.net.Uri, int, int, int, android.os.IBinder); method public int checkUriPermission(android.net.Uri, java.lang.String, java.lang.String, int, int, int); method public void clearWallpaper(); - method public android.content.Context createApplicationContext(android.content.pm.ApplicationInfo, int) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.Context createConfigurationContext(android.content.res.Configuration); method public android.content.Context createContextForSplit(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; - method public android.content.Context createCredentialProtectedStorageContext(); method public android.content.Context createDeviceProtectedStorageContext(); method public android.content.Context createDisplayContext(android.view.Display); method public android.content.Context createPackageContext(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public android.content.Context createPackageContextAsUser(java.lang.String, int, android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; method public java.lang.String[] databaseList(); method public boolean deleteDatabase(java.lang.String); method public boolean deleteFile(java.lang.String); @@ -68,7 +56,6 @@ package android.test.mock { method public android.content.Context getApplicationContext(); method public android.content.pm.ApplicationInfo getApplicationInfo(); method public android.content.res.AssetManager getAssets(); - method public java.lang.String getBasePackageName(); method public java.io.File getCacheDir(); method public java.lang.ClassLoader getClassLoader(); method public java.io.File getCodeCacheDir(); @@ -76,8 +63,6 @@ package android.test.mock { method public java.io.File getDataDir(); method public java.io.File getDatabasePath(java.lang.String); method public java.io.File getDir(java.lang.String, int); - method public android.view.Display getDisplay(); - method public android.view.DisplayAdjustments getDisplayAdjustments(int); method public java.io.File getExternalCacheDir(); method public java.io.File[] getExternalCacheDirs(); method public java.io.File getExternalFilesDir(java.lang.String); @@ -89,25 +74,19 @@ package android.test.mock { method public java.io.File getNoBackupFilesDir(); method public java.io.File getObbDir(); method public java.io.File[] getObbDirs(); - method public java.lang.String getOpPackageName(); method public java.lang.String getPackageCodePath(); method public android.content.pm.PackageManager getPackageManager(); method public java.lang.String getPackageName(); method public java.lang.String getPackageResourcePath(); - method public java.io.File getPreloadsFileCache(); method public android.content.res.Resources getResources(); method public android.content.SharedPreferences getSharedPreferences(java.lang.String, int); - method public android.content.SharedPreferences getSharedPreferences(java.io.File, int); - method public java.io.File getSharedPreferencesPath(java.lang.String); method public java.lang.Object getSystemService(java.lang.String); method public java.lang.String getSystemServiceName(java.lang.Class<?>); method public android.content.res.Resources.Theme getTheme(); - method public int getUserId(); method public android.graphics.drawable.Drawable getWallpaper(); method public int getWallpaperDesiredMinimumHeight(); method public int getWallpaperDesiredMinimumWidth(); method public void grantUriPermission(java.lang.String, android.net.Uri, int); - method public boolean isCredentialProtectedStorage(); method public boolean isDeviceProtectedStorage(); method public boolean moveDatabaseFrom(android.content.Context, java.lang.String); method public boolean moveSharedPreferencesFrom(android.content.Context, java.lang.String); @@ -120,31 +99,19 @@ package android.test.mock { method public android.content.Intent registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter, int); method public android.content.Intent registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter, java.lang.String, android.os.Handler); method public android.content.Intent registerReceiver(android.content.BroadcastReceiver, android.content.IntentFilter, java.lang.String, android.os.Handler, int); - method public android.content.Intent registerReceiverAsUser(android.content.BroadcastReceiver, android.os.UserHandle, android.content.IntentFilter, java.lang.String, android.os.Handler); - method public void reloadSharedPreferences(); method public void removeStickyBroadcast(android.content.Intent); method public void removeStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle); method public void revokeUriPermission(android.net.Uri, int); method public void revokeUriPermission(java.lang.String, android.net.Uri, int); method public void sendBroadcast(android.content.Intent); method public void sendBroadcast(android.content.Intent, java.lang.String); - method public void sendBroadcast(android.content.Intent, java.lang.String, android.os.Bundle); - method public void sendBroadcast(android.content.Intent, java.lang.String, int); method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle); method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle, java.lang.String); - method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle, java.lang.String, android.os.Bundle); - method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle, java.lang.String, int); - method public void sendBroadcastMultiplePermissions(android.content.Intent, java.lang.String[]); method public void sendOrderedBroadcast(android.content.Intent, java.lang.String); method public void sendOrderedBroadcast(android.content.Intent, java.lang.String, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); - method public void sendOrderedBroadcast(android.content.Intent, java.lang.String, android.os.Bundle, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); - method public void sendOrderedBroadcast(android.content.Intent, java.lang.String, int, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); method public void sendOrderedBroadcastAsUser(android.content.Intent, android.os.UserHandle, java.lang.String, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); - method public void sendOrderedBroadcastAsUser(android.content.Intent, android.os.UserHandle, java.lang.String, int, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); - method public void sendOrderedBroadcastAsUser(android.content.Intent, android.os.UserHandle, java.lang.String, int, android.os.Bundle, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); method public void sendStickyBroadcast(android.content.Intent); method public void sendStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle); - method public void sendStickyBroadcastAsUser(android.content.Intent, android.os.UserHandle, android.os.Bundle); method public void sendStickyOrderedBroadcast(android.content.Intent, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); method public void sendStickyOrderedBroadcastAsUser(android.content.Intent, android.os.UserHandle, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); method public void setTheme(int); @@ -155,17 +122,13 @@ package android.test.mock { method public void startActivity(android.content.Intent); method public void startActivity(android.content.Intent, android.os.Bundle); method public android.content.ComponentName startForegroundService(android.content.Intent); - method public android.content.ComponentName startForegroundServiceAsUser(android.content.Intent, android.os.UserHandle); method public boolean startInstrumentation(android.content.ComponentName, java.lang.String, android.os.Bundle); method public void startIntentSender(android.content.IntentSender, android.content.Intent, int, int, int) throws android.content.IntentSender.SendIntentException; method public void startIntentSender(android.content.IntentSender, android.content.Intent, int, int, int, android.os.Bundle) throws android.content.IntentSender.SendIntentException; method public android.content.ComponentName startService(android.content.Intent); - method public android.content.ComponentName startServiceAsUser(android.content.Intent, android.os.UserHandle); method public boolean stopService(android.content.Intent); - method public boolean stopServiceAsUser(android.content.Intent, android.os.UserHandle); method public void unbindService(android.content.ServiceConnection); method public void unregisterReceiver(android.content.BroadcastReceiver); - method public void updateDisplay(int); } public deprecated class MockCursor implements android.database.Cursor { @@ -221,8 +184,6 @@ package android.test.mock { public deprecated class MockPackageManager extends android.content.pm.PackageManager { ctor public MockPackageManager(); - method public void addCrossProfileIntentFilter(android.content.IntentFilter, int, int, int); - method public void addOnPermissionsChangeListener(android.content.pm.PackageManager.OnPermissionsChangedListener); method public void addPackageToPreferred(java.lang.String); method public boolean addPermission(android.content.pm.PermissionInfo); method public boolean addPermissionAsync(android.content.pm.PermissionInfo); @@ -232,19 +193,10 @@ package android.test.mock { method public int checkPermission(java.lang.String, java.lang.String); method public int checkSignatures(java.lang.String, java.lang.String); method public int checkSignatures(int, int); - method public void clearApplicationUserData(java.lang.String, android.content.pm.IPackageDataObserver); - method public void clearCrossProfileIntentFilters(int); method public void clearInstantAppCookie(); method public void clearPackagePreferredActivities(java.lang.String); method public java.lang.String[] currentToCanonicalPackageNames(java.lang.String[]); - method public void deleteApplicationCacheFiles(java.lang.String, android.content.pm.IPackageDataObserver); - method public void deleteApplicationCacheFilesAsUser(java.lang.String, int, android.content.pm.IPackageDataObserver); - method public void deletePackage(java.lang.String, android.content.pm.IPackageDeleteObserver, int); - method public void deletePackageAsUser(java.lang.String, android.content.pm.IPackageDeleteObserver, int, int); method public void extendVerificationTimeout(int, int, long); - method public void flushPackageRestrictionsAsUser(int); - method public void freeStorage(java.lang.String, long, android.content.IntentSender); - method public void freeStorageAndNotify(java.lang.String, long, android.content.pm.IPackageDataObserver); method public android.graphics.drawable.Drawable getActivityBanner(android.content.ComponentName) throws android.content.pm.PackageManager.NameNotFoundException; method public android.graphics.drawable.Drawable getActivityBanner(android.content.Intent) throws android.content.pm.PackageManager.NameNotFoundException; method public android.graphics.drawable.Drawable getActivityIcon(android.content.ComponentName) throws android.content.pm.PackageManager.NameNotFoundException; @@ -257,148 +209,75 @@ package android.test.mock { method public android.graphics.drawable.Drawable getApplicationBanner(android.content.pm.ApplicationInfo); method public android.graphics.drawable.Drawable getApplicationBanner(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; method public int getApplicationEnabledSetting(java.lang.String); - method public boolean getApplicationHiddenSettingAsUser(java.lang.String, android.os.UserHandle); method public android.graphics.drawable.Drawable getApplicationIcon(android.content.pm.ApplicationInfo); method public android.graphics.drawable.Drawable getApplicationIcon(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.pm.ApplicationInfo getApplicationInfo(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public android.content.pm.ApplicationInfo getApplicationInfoAsUser(java.lang.String, int, int) throws android.content.pm.PackageManager.NameNotFoundException; method public java.lang.CharSequence getApplicationLabel(android.content.pm.ApplicationInfo); method public android.graphics.drawable.Drawable getApplicationLogo(android.content.pm.ApplicationInfo); method public android.graphics.drawable.Drawable getApplicationLogo(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.pm.ChangedPackages getChangedPackages(int); method public int getComponentEnabledSetting(android.content.ComponentName); method public android.graphics.drawable.Drawable getDefaultActivityIcon(); - method public java.lang.String getDefaultBrowserPackageNameAsUser(int); method public android.graphics.drawable.Drawable getDrawable(java.lang.String, int, android.content.pm.ApplicationInfo); - method public android.content.ComponentName getHomeActivities(java.util.List<android.content.pm.ResolveInfo>); - method public int getInstallReason(java.lang.String, android.os.UserHandle); method public java.util.List<android.content.pm.ApplicationInfo> getInstalledApplications(int); - method public java.util.List<android.content.pm.ApplicationInfo> getInstalledApplicationsAsUser(int, int); method public java.util.List<android.content.pm.PackageInfo> getInstalledPackages(int); - method public java.util.List<android.content.pm.PackageInfo> getInstalledPackagesAsUser(int, int); method public java.lang.String getInstallerPackageName(java.lang.String); - method public java.lang.String getInstantAppAndroidId(java.lang.String, android.os.UserHandle); method public byte[] getInstantAppCookie(); method public int getInstantAppCookieMaxBytes(); - method public int getInstantAppCookieMaxSize(); - method public android.graphics.drawable.Drawable getInstantAppIcon(java.lang.String); - method public android.content.ComponentName getInstantAppInstallerComponent(); - method public android.content.ComponentName getInstantAppResolverSettingsComponent(); - method public java.util.List<android.content.pm.InstantAppInfo> getInstantApps(); method public android.content.pm.InstrumentationInfo getInstrumentationInfo(android.content.ComponentName, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public java.util.List<android.content.pm.IntentFilterVerificationInfo> getIntentFilterVerifications(java.lang.String); - method public int getIntentVerificationStatusAsUser(java.lang.String, int); - method public android.content.pm.KeySet getKeySetByAlias(java.lang.String, java.lang.String); method public android.content.Intent getLaunchIntentForPackage(java.lang.String); method public android.content.Intent getLeanbackLaunchIntentForPackage(java.lang.String); - method public int getMoveStatus(int); method public java.lang.String getNameForUid(int); - method public java.lang.String[] getNamesForUids(int[]); - method public java.util.List<android.os.storage.VolumeInfo> getPackageCandidateVolumes(android.content.pm.ApplicationInfo); - method public android.os.storage.VolumeInfo getPackageCurrentVolume(android.content.pm.ApplicationInfo); method public int[] getPackageGids(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; method public int[] getPackageGids(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.pm.PackageInfo getPackageInfo(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.pm.PackageInfo getPackageInfo(android.content.pm.VersionedPackage, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public android.content.pm.PackageInfo getPackageInfoAsUser(java.lang.String, int, int) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.pm.PackageInstaller getPackageInstaller(); - method public void getPackageSizeInfoAsUser(java.lang.String, int, android.content.pm.IPackageStatsObserver); method public int getPackageUid(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public int getPackageUidAsUser(java.lang.String, int, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public int getPackageUidAsUser(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; method public java.lang.String[] getPackagesForUid(int); method public java.util.List<android.content.pm.PackageInfo> getPackagesHoldingPermissions(java.lang.String[], int); - method public java.lang.String getPermissionControllerPackageName(); - method public int getPermissionFlags(java.lang.String, java.lang.String, android.os.UserHandle); method public android.content.pm.PermissionGroupInfo getPermissionGroupInfo(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.pm.PermissionInfo getPermissionInfo(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; method public int getPreferredActivities(java.util.List<android.content.IntentFilter>, java.util.List<android.content.ComponentName>, java.lang.String); method public java.util.List<android.content.pm.PackageInfo> getPreferredPackages(int); - method public java.util.List<android.os.storage.VolumeInfo> getPrimaryStorageCandidateVolumes(); - method public android.os.storage.VolumeInfo getPrimaryStorageCurrentVolume(); method public android.content.pm.ProviderInfo getProviderInfo(android.content.ComponentName, int) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.pm.ActivityInfo getReceiverInfo(android.content.ComponentName, int) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.res.Resources getResourcesForActivity(android.content.ComponentName) throws android.content.pm.PackageManager.NameNotFoundException; method public android.content.res.Resources getResourcesForApplication(android.content.pm.ApplicationInfo); method public android.content.res.Resources getResourcesForApplication(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; - method public android.content.res.Resources getResourcesForApplicationAsUser(java.lang.String, int); method public android.content.pm.ServiceInfo getServiceInfo(android.content.ComponentName, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public java.lang.String getServicesSystemSharedLibraryPackageName(); method public java.util.List<android.content.pm.SharedLibraryInfo> getSharedLibraries(int); - method public java.util.List<android.content.pm.SharedLibraryInfo> getSharedLibrariesAsUser(int, int); - method public java.lang.String getSharedSystemSharedLibraryPackageName(); - method public android.content.pm.KeySet getSigningKeySet(java.lang.String); method public android.content.pm.FeatureInfo[] getSystemAvailableFeatures(); method public java.lang.String[] getSystemSharedLibraryNames(); method public java.lang.CharSequence getText(java.lang.String, int, android.content.pm.ApplicationInfo); - method public int getUidForSharedUser(java.lang.String); - method public android.graphics.drawable.Drawable getUserBadgeForDensity(android.os.UserHandle, int); - method public android.graphics.drawable.Drawable getUserBadgeForDensityNoBackground(android.os.UserHandle, int); method public android.graphics.drawable.Drawable getUserBadgedDrawableForDensity(android.graphics.drawable.Drawable, android.os.UserHandle, android.graphics.Rect, int); method public android.graphics.drawable.Drawable getUserBadgedIcon(android.graphics.drawable.Drawable, android.os.UserHandle); method public java.lang.CharSequence getUserBadgedLabel(java.lang.CharSequence, android.os.UserHandle); - method public android.content.pm.VerifierDeviceIdentity getVerifierDeviceIdentity(); method public android.content.res.XmlResourceParser getXml(java.lang.String, int, android.content.pm.ApplicationInfo); - method public void grantRuntimePermission(java.lang.String, java.lang.String, android.os.UserHandle); method public boolean hasSystemFeature(java.lang.String); method public boolean hasSystemFeature(java.lang.String, int); - method public int installExistingPackage(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; - method public int installExistingPackage(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public int installExistingPackageAsUser(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public void installPackage(android.net.Uri, android.app.PackageInstallObserver, int, java.lang.String); method public boolean isInstantApp(); method public boolean isInstantApp(java.lang.String); - method public boolean isPackageAvailable(java.lang.String); - method public boolean isPackageSuspendedForUser(java.lang.String, int); - method public boolean isPermissionReviewModeEnabled(); method public boolean isPermissionRevokedByPolicy(java.lang.String, java.lang.String); method public boolean isSafeMode(); - method public boolean isSignedBy(java.lang.String, android.content.pm.KeySet); - method public boolean isSignedByExactly(java.lang.String, android.content.pm.KeySet); - method public boolean isUpgrade(); - method public android.graphics.drawable.Drawable loadItemIcon(android.content.pm.PackageItemInfo, android.content.pm.ApplicationInfo); - method public android.graphics.drawable.Drawable loadUnbadgedItemIcon(android.content.pm.PackageItemInfo, android.content.pm.ApplicationInfo); - method public int movePackage(java.lang.String, android.os.storage.VolumeInfo); - method public int movePrimaryStorage(android.os.storage.VolumeInfo); method public java.util.List<android.content.pm.ResolveInfo> queryBroadcastReceivers(android.content.Intent, int); - method public java.util.List<android.content.pm.ResolveInfo> queryBroadcastReceiversAsUser(android.content.Intent, int, int); method public java.util.List<android.content.pm.ProviderInfo> queryContentProviders(java.lang.String, int, int); method public java.util.List<android.content.pm.InstrumentationInfo> queryInstrumentation(java.lang.String, int); method public java.util.List<android.content.pm.ResolveInfo> queryIntentActivities(android.content.Intent, int); - method public java.util.List<android.content.pm.ResolveInfo> queryIntentActivitiesAsUser(android.content.Intent, int, int); method public java.util.List<android.content.pm.ResolveInfo> queryIntentActivityOptions(android.content.ComponentName, android.content.Intent[], android.content.Intent, int); method public java.util.List<android.content.pm.ResolveInfo> queryIntentContentProviders(android.content.Intent, int); - method public java.util.List<android.content.pm.ResolveInfo> queryIntentContentProvidersAsUser(android.content.Intent, int, int); method public java.util.List<android.content.pm.ResolveInfo> queryIntentServices(android.content.Intent, int); - method public java.util.List<android.content.pm.ResolveInfo> queryIntentServicesAsUser(android.content.Intent, int, int); method public java.util.List<android.content.pm.PermissionInfo> queryPermissionsByGroup(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; - method public void registerDexModule(java.lang.String, android.content.pm.PackageManager.DexModuleRegisterCallback); - method public void registerMoveCallback(android.content.pm.PackageManager.MoveCallback, android.os.Handler); - method public void removeOnPermissionsChangeListener(android.content.pm.PackageManager.OnPermissionsChangedListener); method public void removePackageFromPreferred(java.lang.String); method public void removePermission(java.lang.String); - method public void replacePreferredActivity(android.content.IntentFilter, int, android.content.ComponentName[], android.content.ComponentName); method public android.content.pm.ResolveInfo resolveActivity(android.content.Intent, int); - method public android.content.pm.ResolveInfo resolveActivityAsUser(android.content.Intent, int, int); method public android.content.pm.ProviderInfo resolveContentProvider(java.lang.String, int); - method public android.content.pm.ProviderInfo resolveContentProviderAsUser(java.lang.String, int, int); method public android.content.pm.ResolveInfo resolveService(android.content.Intent, int); - method public void revokeRuntimePermission(java.lang.String, java.lang.String, android.os.UserHandle); method public void setApplicationCategoryHint(java.lang.String, int); method public void setApplicationEnabledSetting(java.lang.String, int, int); - method public boolean setApplicationHiddenSettingAsUser(java.lang.String, boolean, android.os.UserHandle); method public void setComponentEnabledSetting(android.content.ComponentName, int, int); - method public boolean setDefaultBrowserPackageNameAsUser(java.lang.String, int); method public void setInstallerPackageName(java.lang.String, java.lang.String); - method public boolean setInstantAppCookie(byte[]); - method public java.lang.String[] setPackagesSuspendedAsUser(java.lang.String[], boolean, int); - method public void setUpdateAvailable(java.lang.String, boolean); - method public boolean shouldShowRequestPermissionRationale(java.lang.String); - method public void unregisterMoveCallback(android.content.pm.PackageManager.MoveCallback); method public void updateInstantAppCookie(byte[]); - method public boolean updateIntentVerificationStatusAsUser(java.lang.String, int, int); - method public void updatePermissionFlags(java.lang.String, java.lang.String, int, int, android.os.UserHandle); - method public void verifyIntentFilter(int, int, java.util.List<java.lang.String>); method public void verifyPendingInstall(int, int); } diff --git a/test-mock/api/android-test-mock-removed.txt b/test-mock/api/android-test-mock-removed.txt index 5b358cfdbf59..bd109a887933 100644 --- a/test-mock/api/android-test-mock-removed.txt +++ b/test-mock/api/android-test-mock-removed.txt @@ -8,6 +8,7 @@ package android.test.mock { public deprecated class MockPackageManager extends android.content.pm.PackageManager { method public deprecated java.lang.String getDefaultBrowserPackageName(int); method public deprecated boolean setDefaultBrowserPackageName(java.lang.String, int); + method public boolean setInstantAppCookie(byte[]); } } diff --git a/test-mock/api/android-test-mock-system-current.txt b/test-mock/api/android-test-mock-system-current.txt new file mode 100644 index 000000000000..20401a50b6a2 --- /dev/null +++ b/test-mock/api/android-test-mock-system-current.txt @@ -0,0 +1,38 @@ +package android.test.mock { + + public class MockContext extends android.content.Context { + method public android.content.Context createCredentialProtectedStorageContext(); + method public java.io.File getPreloadsFileCache(); + method public boolean isCredentialProtectedStorage(); + method public void sendBroadcast(android.content.Intent, java.lang.String, android.os.Bundle); + method public void sendBroadcastAsUser(android.content.Intent, android.os.UserHandle, java.lang.String, android.os.Bundle); + method public void sendOrderedBroadcast(android.content.Intent, java.lang.String, android.os.Bundle, android.content.BroadcastReceiver, android.os.Handler, int, java.lang.String, android.os.Bundle); + } + + public deprecated class MockPackageManager extends android.content.pm.PackageManager { + method public void addOnPermissionsChangeListener(android.content.pm.PackageManager.OnPermissionsChangedListener); + method public java.util.List<android.content.IntentFilter> getAllIntentFilters(java.lang.String); + method public java.lang.String getDefaultBrowserPackageNameAsUser(int); + method public java.util.List<android.content.pm.PackageInfo> getInstalledPackagesAsUser(int, int); + method public android.graphics.drawable.Drawable getInstantAppIcon(java.lang.String); + method public android.content.ComponentName getInstantAppInstallerComponent(); + method public android.content.ComponentName getInstantAppResolverSettingsComponent(); + method public java.util.List<android.content.pm.InstantAppInfo> getInstantApps(); + method public java.util.List<android.content.pm.IntentFilterVerificationInfo> getIntentFilterVerifications(java.lang.String); + method public int getIntentVerificationStatusAsUser(java.lang.String, int); + method public int getPermissionFlags(java.lang.String, java.lang.String, android.os.UserHandle); + method public void grantRuntimePermission(java.lang.String, java.lang.String, android.os.UserHandle); + method public int installExistingPackage(java.lang.String) throws android.content.pm.PackageManager.NameNotFoundException; + method public int installExistingPackage(java.lang.String, int) throws android.content.pm.PackageManager.NameNotFoundException; + method public void registerDexModule(java.lang.String, android.content.pm.PackageManager.DexModuleRegisterCallback); + method public void removeOnPermissionsChangeListener(android.content.pm.PackageManager.OnPermissionsChangedListener); + method public void revokeRuntimePermission(java.lang.String, java.lang.String, android.os.UserHandle); + method public boolean setDefaultBrowserPackageNameAsUser(java.lang.String, int); + method public void setUpdateAvailable(java.lang.String, boolean); + method public boolean updateIntentVerificationStatusAsUser(java.lang.String, int, int); + method public void updatePermissionFlags(java.lang.String, java.lang.String, int, int, android.os.UserHandle); + method public void verifyIntentFilter(int, int, java.util.List<java.lang.String>); + } + +} + diff --git a/test-mock/api/android-test-mock-system-removed.txt b/test-mock/api/android-test-mock-system-removed.txt new file mode 100644 index 000000000000..e69de29bb2d1 --- /dev/null +++ b/test-mock/api/android-test-mock-system-removed.txt diff --git a/test-mock/api/apicheck_msg_android_test_mock-system.txt b/test-mock/api/apicheck_msg_android_test_mock-system.txt new file mode 100644 index 000000000000..3a97117f3ea1 --- /dev/null +++ b/test-mock/api/apicheck_msg_android_test_mock-system.txt @@ -0,0 +1,17 @@ + +****************************** +You have tried to change the API from what has been previously approved. + +To make these errors go away, you have two choices: + 1) You can add "@hide" javadoc comments to the methods, etc. listed in the + errors above. + + 2) You can update android-test-mock-current.txt by executing the following command: + make update-android-test-mock-system-api + + To submit the revised android-test-mock-system-current.txt to the main Android repository, + you will need approval. +****************************** + + + diff --git a/tests/AppLaunch/Android.mk b/tests/AppLaunch/Android.mk index 09739e5e074a..917293fa266b 100644 --- a/tests/AppLaunch/Android.mk +++ b/tests/AppLaunch/Android.mk @@ -13,6 +13,8 @@ LOCAL_JAVA_LIBRARIES := android.test.base android.test.runner LOCAL_STATIC_JAVA_LIBRARIES := android-support-test +LOCAL_COMPATIBILITY_SUITE := device-tests + include $(BUILD_PACKAGE) # Use the following include to make our test apk. diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index 0f6fb50aba48..5831875680ac 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -294,36 +294,42 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& printer->Print("/"); printer->Print(entry->name); - switch (entry->symbol_status.state) { - case SymbolState::kPublic: + switch (entry->visibility.level) { + case Visibility::Level::kPublic: printer->Print(" PUBLIC"); break; - case SymbolState::kPrivate: + case Visibility::Level::kPrivate: printer->Print(" _PRIVATE_"); break; - case SymbolState::kUndefined: + case Visibility::Level::kUndefined: // Print nothing. break; } + if (entry->overlayable) { + printer->Print(" OVERLAYABLE"); + } + printer->Println(); - printer->Indent(); - for (const auto& value : entry->values) { - printer->Print("("); - printer->Print(value->config.to_string()); - printer->Print(") "); - value->value->Accept(&headline_printer); - if (options.show_sources && !value->value->GetSource().path.empty()) { - printer->Print(" src="); - printer->Print(value->value->GetSource().to_string()); - } - printer->Println(); + if (options.show_values) { printer->Indent(); - value->value->Accept(&body_printer); + for (const auto& value : entry->values) { + printer->Print("("); + printer->Print(value->config.to_string()); + printer->Print(") "); + value->value->Accept(&headline_printer); + if (options.show_sources && !value->value->GetSource().path.empty()) { + printer->Print(" src="); + printer->Print(value->value->GetSource().to_string()); + } + printer->Println(); + printer->Indent(); + value->value->Accept(&body_printer); + printer->Undent(); + } printer->Undent(); } - printer->Undent(); } printer->Undent(); } diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h index 3c1ee4c0cdba..6209a04c3c2d 100644 --- a/tools/aapt2/Debug.h +++ b/tools/aapt2/Debug.h @@ -29,6 +29,7 @@ namespace aapt { struct DebugPrintTableOptions { bool show_sources = false; + bool show_values = true; }; struct Debug { diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index 4cc60a8dbb07..24b28dd7d970 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -30,7 +30,7 @@ #include "util/Util.h" #include "xml/XmlPullParser.h" -using android::StringPiece; +using ::android::StringPiece; namespace aapt { @@ -90,9 +90,12 @@ struct ParsedResource { ConfigDescription config; std::string product; Source source; + ResourceId id; - Maybe<SymbolState> symbol_state; + Visibility::Level visibility_level = Visibility::Level::kUndefined; bool allow_new = false; + bool overlayable = false; + std::string comment; std::unique_ptr<Value> value; std::list<ParsedResource> child_resources; @@ -106,24 +109,41 @@ static bool AddResourcesToTable(ResourceTable* table, IDiagnostics* diag, Parsed res->comment = trimmed_comment.to_string(); } - if (res->symbol_state) { - Symbol symbol; - symbol.state = res->symbol_state.value(); - symbol.source = res->source; - symbol.comment = res->comment; - symbol.allow_new = res->allow_new; - if (!table->SetSymbolState(res->name, res->id, symbol, diag)) { + if (res->visibility_level != Visibility::Level::kUndefined) { + Visibility visibility; + visibility.level = res->visibility_level; + visibility.source = res->source; + visibility.comment = res->comment; + if (!table->SetVisibilityWithId(res->name, visibility, res->id, diag)) { + return false; + } + } + + if (res->allow_new) { + AllowNew allow_new; + allow_new.source = res->source; + allow_new.comment = res->comment; + if (!table->SetAllowNew(res->name, allow_new, diag)) { return false; } } - if (res->value) { + if (res->overlayable) { + Overlayable overlayable; + overlayable.source = res->source; + overlayable.comment = res->comment; + if (!table->SetOverlayable(res->name, overlayable, diag)) { + return false; + } + } + + if (res->value != nullptr) { // Attach the comment, source and config to the value. res->value->SetComment(std::move(res->comment)); res->value->SetSource(std::move(res->source)); - if (!table->AddResource(res->name, res->id, res->config, res->product, std::move(res->value), - diag)) { + if (!table->AddResourceWithId(res->name, res->id, res->config, res->product, + std::move(res->value), diag)) { return false; } } @@ -601,8 +621,7 @@ std::unique_ptr<Item> ResourceParser::ParseXml(xml::XmlPullParser* parser, // Process the raw value. std::unique_ptr<Item> processed_item = - ResourceUtils::TryParseItemForAttribute(raw_value, type_mask, - on_create_reference); + ResourceUtils::TryParseItemForAttribute(raw_value, type_mask, on_create_reference); if (processed_item) { // Fix up the reference. if (Reference* ref = ValueCast<Reference>(processed_item.get())) { @@ -689,8 +708,7 @@ bool ResourceParser::ParseString(xml::XmlPullParser* parser, return true; } -bool ResourceParser::ParsePublic(xml::XmlPullParser* parser, - ParsedResource* out_resource) { +bool ResourceParser::ParsePublic(xml::XmlPullParser* parser, ParsedResource* out_resource) { if (out_resource->config != ConfigDescription::DefaultConfig()) { diag_->Warn(DiagMessage(out_resource->source) << "ignoring configuration '" << out_resource->config << "' for <public> tag"); @@ -728,7 +746,7 @@ bool ResourceParser::ParsePublic(xml::XmlPullParser* parser, out_resource->value = util::make_unique<Id>(); } - out_resource->symbol_state = SymbolState::kPublic; + out_resource->visibility_level = Visibility::Level::kPublic; return true; } @@ -818,7 +836,7 @@ bool ResourceParser::ParsePublicGroup(xml::XmlPullParser* parser, ParsedResource child_resource.id = next_id; child_resource.comment = std::move(comment); child_resource.source = item_source; - child_resource.symbol_state = SymbolState::kPublic; + child_resource.visibility_level = Visibility::Level::kPublic; out_resource->child_resources.push_back(std::move(child_resource)); next_id.id += 1; @@ -864,7 +882,7 @@ bool ResourceParser::ParseSymbol(xml::XmlPullParser* parser, ParsedResource* out return false; } - out_resource->symbol_state = SymbolState::kPrivate; + out_resource->visibility_level = Visibility::Level::kPrivate; return true; } @@ -920,8 +938,12 @@ bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource continue; } - // TODO(b/64980941): Mark the symbol as overlayable and allow marking which entity can overlay - // the resource (system/app). + ParsedResource child_resource; + child_resource.name.type = *type; + child_resource.name.entry = maybe_name.value().to_string(); + child_resource.source = item_source; + child_resource.overlayable = true; + out_resource->child_resources.push_back(std::move(child_resource)); xml::XmlPullParser::SkipCurrentElement(parser); } else if (!ShouldIgnoreElement(element_namespace, element_name)) { @@ -932,10 +954,9 @@ bool ResourceParser::ParseOverlayable(xml::XmlPullParser* parser, ParsedResource return !error; } -bool ResourceParser::ParseAddResource(xml::XmlPullParser* parser, - ParsedResource* out_resource) { +bool ResourceParser::ParseAddResource(xml::XmlPullParser* parser, ParsedResource* out_resource) { if (ParseSymbolImpl(parser, out_resource)) { - out_resource->symbol_state = SymbolState::kUndefined; + out_resource->visibility_level = Visibility::Level::kUndefined; out_resource->allow_new = true; return true; } @@ -1395,9 +1416,8 @@ bool ResourceParser::ParseDeclareStyleable(xml::XmlPullParser* parser, ParsedResource* out_resource) { out_resource->name.type = ResourceType::kStyleable; - // Declare-styleable is kPrivate by default, because it technically only - // exists in R.java. - out_resource->symbol_state = SymbolState::kPublic; + // Declare-styleable is kPrivate by default, because it technically only exists in R.java. + out_resource->visibility_level = Visibility::Level::kPublic; // Declare-styleable only ends up in default config; if (out_resource->config != ConfigDescription::DefaultConfig()) { diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp index 9a5cd3edb47f..618c8ed4afd1 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -29,8 +29,8 @@ using ::aapt::io::StringInputStream; using ::aapt::test::StrValueEq; using ::aapt::test::ValueEq; -using ::android::ResTable_map; using ::android::Res_value; +using ::android::ResTable_map; using ::android::StringPiece; using ::testing::Eq; using ::testing::IsEmpty; @@ -38,6 +38,7 @@ using ::testing::IsNull; using ::testing::NotNull; using ::testing::Pointee; using ::testing::SizeIs; +using ::testing::StrEq; namespace aapt { @@ -482,7 +483,7 @@ TEST_F(ResourceParserTest, ParseAttributesDeclareStyleable) { Maybe<ResourceTable::SearchResult> result = table_.FindResource(test::ParseNameOrDie("styleable/foo")); ASSERT_TRUE(result); - EXPECT_THAT(result.value().entry->symbol_status.state, Eq(SymbolState::kPublic)); + EXPECT_THAT(result.value().entry->visibility.level, Eq(Visibility::Level::kPublic)); Attribute* attr = test::GetValue<Attribute>(&table_, "attr/bar"); ASSERT_THAT(attr, NotNull()); @@ -718,6 +719,26 @@ TEST_F(ResourceParserTest, AutoIncrementIdsInPublicGroup) { EXPECT_THAT(actual_id, Eq(ResourceId(0x01010041))); } +TEST_F(ResourceParserTest, StrongestSymbolVisibilityWins) { + std::string input = R"( + <!-- private --> + <java-symbol type="string" name="foo" /> + <!-- public --> + <public type="string" name="foo" id="0x01020000" /> + <!-- private2 --> + <java-symbol type="string" name="foo" />)"; + ASSERT_TRUE(TestParse(input)); + + Maybe<ResourceTable::SearchResult> result = + table_.FindResource(test::ParseNameOrDie("string/foo")); + ASSERT_TRUE(result); + + ResourceEntry* entry = result.value().entry; + ASSERT_THAT(entry, NotNull()); + EXPECT_THAT(entry->visibility.level, Eq(Visibility::Level::kPublic)); + EXPECT_THAT(entry->visibility.comment, StrEq("public")); +} + TEST_F(ResourceParserTest, ExternalTypesShouldOnlyBeReferences) { ASSERT_TRUE(TestParse(R"(<item type="layout" name="foo">@layout/bar</item>)")); ASSERT_FALSE(TestParse(R"(<item type="layout" name="bar">"this is a string"</item>)")); @@ -731,8 +752,8 @@ TEST_F(ResourceParserTest, AddResourcesElementShouldAddEntryWithUndefinedSymbol) ASSERT_TRUE(result); const ResourceEntry* entry = result.value().entry; ASSERT_THAT(entry, NotNull()); - EXPECT_THAT(entry->symbol_status.state, Eq(SymbolState::kUndefined)); - EXPECT_TRUE(entry->symbol_status.allow_new); + EXPECT_THAT(entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_TRUE(entry->allow_new); } TEST_F(ResourceParserTest, ParseItemElementWithFormat) { @@ -833,6 +854,22 @@ TEST_F(ResourceParserTest, ParseOverlayableTagWithSystemPolicy) { <item type="string" name="bar" /> </overlayable>)"; ASSERT_TRUE(TestParse(input)); + + Maybe<ResourceTable::SearchResult> search_result = + table_.FindResource(test::ParseNameOrDie("string/bar")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_THAT(search_result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_TRUE(search_result.value().entry->overlayable); +} + +TEST_F(ResourceParserTest, DuplicateOverlayableIsError) { + std::string input = R"( + <overlayable> + <item type="string" name="foo" /> + <item type="string" name="foo" /> + </overlayable>)"; + EXPECT_FALSE(TestParse(input)); } } // namespace aapt diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index 0304e21698df..3172892d7172 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -22,6 +22,7 @@ #include <tuple> #include "android-base/logging.h" +#include "android-base/stringprintf.h" #include "androidfw/ResourceTypes.h" #include "ConfigDescription.h" @@ -33,6 +34,7 @@ using ::aapt::text::IsValidResourceEntryName; using ::android::StringPiece; +using ::android::base::StringPrintf; namespace aapt { @@ -45,7 +47,7 @@ static bool less_than_struct_with_name(const std::unique_ptr<T>& lhs, const Stri return lhs->name.compare(0, lhs->name.size(), rhs.data(), rhs.size()) < 0; } -ResourceTablePackage* ResourceTable::FindPackage(const StringPiece& name) { +ResourceTablePackage* ResourceTable::FindPackage(const StringPiece& name) const { const auto last = packages.end(); auto iter = std::lower_bound(packages.begin(), last, name, less_than_struct_with_name<ResourceTablePackage>); @@ -55,7 +57,7 @@ ResourceTablePackage* ResourceTable::FindPackage(const StringPiece& name) { return nullptr; } -ResourceTablePackage* ResourceTable::FindPackageById(uint8_t id) { +ResourceTablePackage* ResourceTable::FindPackageById(uint8_t id) const { for (auto& package : packages) { if (package->id && package->id.value() == id) { return package.get(); @@ -206,30 +208,23 @@ std::vector<ResourceConfigValue*> ResourceEntry::FindValuesIf( return results; } -/** - * The default handler for collisions. - * - * Typically, a weak value will be overridden by a strong value. An existing - * weak - * value will not be overridden by an incoming weak value. - * - * There are some exceptions: - * - * Attributes: There are two types of Attribute values: USE and DECL. - * - * USE is anywhere an Attribute is declared without a format, and in a place - * that would - * be legal to declare if the Attribute already existed. This is typically in a - * <declare-styleable> tag. Attributes defined in a <declare-styleable> are also - * weak. - * - * DECL is an absolute declaration of an Attribute and specifies an explicit - * format. - * - * A DECL will override a USE without error. Two DECLs must match in their - * format for there to be - * no error. - */ +// The default handler for collisions. +// +// Typically, a weak value will be overridden by a strong value. An existing weak +// value will not be overridden by an incoming weak value. +// +// There are some exceptions: +// +// Attributes: There are two types of Attribute values: USE and DECL. +// +// USE is anywhere an Attribute is declared without a format, and in a place that would +// be legal to declare if the Attribute already existed. This is typically in a +// <declare-styleable> tag. Attributes defined in a <declare-styleable> are also weak. +// +// DECL is an absolute declaration of an Attribute and specifies an explicit format. +// +// A DECL will override a USE without error. Two DECLs must match in their format for there to be +// no error. ResourceTable::CollisionResult ResourceTable::ResolveValueCollision(Value* existing, Value* incoming) { Attribute* existing_attr = ValueCast<Attribute>(existing); @@ -287,14 +282,14 @@ ResourceTable::CollisionResult ResourceTable::ResolveValueCollision(Value* exist return CollisionResult::kConflict; } -static StringPiece ValidateName(const StringPiece& name) { +static StringPiece ResourceNameValidator(const StringPiece& name) { if (!IsValidResourceEntryName(name)) { return name; } return {}; } -static StringPiece SkipValidateName(const StringPiece& /*name*/) { +static StringPiece SkipNameValidator(const StringPiece& /*name*/) { return {}; } @@ -303,17 +298,14 @@ bool ResourceTable::AddResource(const ResourceNameRef& name, const StringPiece& product, std::unique_ptr<Value> value, IDiagnostics* diag) { - return AddResourceImpl(name, {}, config, product, std::move(value), ValidateName, + return AddResourceImpl(name, {}, config, product, std::move(value), ResourceNameValidator, ResolveValueCollision, diag); } -bool ResourceTable::AddResource(const ResourceNameRef& name, - const ResourceId& res_id, - const ConfigDescription& config, - const StringPiece& product, - std::unique_ptr<Value> value, - IDiagnostics* diag) { - return AddResourceImpl(name, res_id, config, product, std::move(value), ValidateName, +bool ResourceTable::AddResourceWithId(const ResourceNameRef& name, const ResourceId& res_id, + const ConfigDescription& config, const StringPiece& product, + std::unique_ptr<Value> value, IDiagnostics* diag) { + return AddResourceImpl(name, res_id, config, product, std::move(value), ResourceNameValidator, ResolveValueCollision, diag); } @@ -322,14 +314,14 @@ bool ResourceTable::AddFileReference(const ResourceNameRef& name, const Source& source, const StringPiece& path, IDiagnostics* diag) { - return AddFileReferenceImpl(name, config, source, path, nullptr, ValidateName, diag); + return AddFileReferenceImpl(name, config, source, path, nullptr, ResourceNameValidator, diag); } -bool ResourceTable::AddFileReferenceAllowMangled( - const ResourceNameRef& name, const ConfigDescription& config, - const Source& source, const StringPiece& path, io::IFile* file, - IDiagnostics* diag) { - return AddFileReferenceImpl(name, config, source, path, file, SkipValidateName, diag); +bool ResourceTable::AddFileReferenceMangled(const ResourceNameRef& name, + const ConfigDescription& config, const Source& source, + const StringPiece& path, io::IFile* file, + IDiagnostics* diag) { + return AddFileReferenceImpl(name, config, source, path, file, SkipNameValidator, diag); } bool ResourceTable::AddFileReferenceImpl(const ResourceNameRef& name, @@ -344,88 +336,85 @@ bool ResourceTable::AddFileReferenceImpl(const ResourceNameRef& name, name_validator, ResolveValueCollision, diag); } -bool ResourceTable::AddResourceAllowMangled(const ResourceNameRef& name, - const ConfigDescription& config, - const StringPiece& product, - std::unique_ptr<Value> value, - IDiagnostics* diag) { - return AddResourceImpl(name, ResourceId{}, config, product, std::move(value), SkipValidateName, +bool ResourceTable::AddResourceMangled(const ResourceNameRef& name, const ConfigDescription& config, + const StringPiece& product, std::unique_ptr<Value> value, + IDiagnostics* diag) { + return AddResourceImpl(name, ResourceId{}, config, product, std::move(value), SkipNameValidator, ResolveValueCollision, diag); } -bool ResourceTable::AddResourceAllowMangled(const ResourceNameRef& name, - const ResourceId& id, - const ConfigDescription& config, - const StringPiece& product, - std::unique_ptr<Value> value, - IDiagnostics* diag) { - return AddResourceImpl(name, id, config, product, std::move(value), SkipValidateName, +bool ResourceTable::AddResourceWithIdMangled(const ResourceNameRef& name, const ResourceId& id, + const ConfigDescription& config, + const StringPiece& product, + std::unique_ptr<Value> value, IDiagnostics* diag) { + return AddResourceImpl(name, id, config, product, std::move(value), SkipNameValidator, ResolveValueCollision, diag); } +bool ResourceTable::ValidateName(NameValidator name_validator, const ResourceNameRef& name, + const Source& source, IDiagnostics* diag) { + const StringPiece bad_char = name_validator(name.entry); + if (!bad_char.empty()) { + diag->Error(DiagMessage(source) << "resource '" << name << "' has invalid entry name '" + << name.entry << "'. Invalid character '" << bad_char << "'"); + return false; + } + return true; +} + bool ResourceTable::AddResourceImpl(const ResourceNameRef& name, const ResourceId& res_id, const ConfigDescription& config, const StringPiece& product, std::unique_ptr<Value> value, NameValidator name_validator, - const CollisionResolverFunc& conflictResolver, + const CollisionResolverFunc& conflict_resolver, IDiagnostics* diag) { CHECK(value != nullptr); CHECK(diag != nullptr); - const StringPiece bad_char = name_validator(name.entry); - if (!bad_char.empty()) { - diag->Error(DiagMessage(value->GetSource()) << "resource '" << name - << "' has invalid entry name '" << name.entry - << "'. Invalid character '" << bad_char << "'"); - + const Source& source = value->GetSource(); + if (!ValidateName(name_validator, name, source, diag)) { return false; } ResourceTablePackage* package = FindOrCreatePackage(name.package); if (res_id.is_valid_dynamic() && package->id && package->id.value() != res_id.package_id()) { - diag->Error(DiagMessage(value->GetSource()) - << "trying to add resource '" << name << "' with ID " << res_id - << " but package '" << package->name << "' already has ID " - << std::hex << (int)package->id.value() << std::dec); + diag->Error(DiagMessage(source) << "trying to add resource '" << name << "' with ID " << res_id + << " but package '" << package->name << "' already has ID " + << StringPrintf("%02x", package->id.value())); return false; } ResourceTableType* type = package->FindOrCreateType(name.type); if (res_id.is_valid_dynamic() && type->id && type->id.value() != res_id.type_id()) { - diag->Error(DiagMessage(value->GetSource()) - << "trying to add resource '" << name << "' with ID " << res_id - << " but type '" << type->type << "' already has ID " - << std::hex << (int)type->id.value() << std::dec); + diag->Error(DiagMessage(source) + << "trying to add resource '" << name << "' with ID " << res_id << " but type '" + << type->type << "' already has ID " << StringPrintf("%02x", type->id.value())); return false; } ResourceEntry* entry = type->FindOrCreateEntry(name.entry); if (res_id.is_valid_dynamic() && entry->id && entry->id.value() != res_id.entry_id()) { - diag->Error(DiagMessage(value->GetSource()) + diag->Error(DiagMessage(source) << "trying to add resource '" << name << "' with ID " << res_id << " but resource already has ID " - << ResourceId(package->id.value(), type->id.value(), - entry->id.value())); + << ResourceId(package->id.value(), type->id.value(), entry->id.value())); return false; } ResourceConfigValue* config_value = entry->FindOrCreateValue(config, product); - if (!config_value->value) { + if (config_value->value == nullptr) { // Resource does not exist, add it now. config_value->value = std::move(value); - } else { - switch (conflictResolver(config_value->value.get(), value.get())) { + switch (conflict_resolver(config_value->value.get(), value.get())) { case CollisionResult::kTakeNew: // Take the incoming value. config_value->value = std::move(value); break; case CollisionResult::kConflict: - diag->Error(DiagMessage(value->GetSource()) - << "duplicate value for resource '" << name << "' " - << "with config '" << config << "'"); - diag->Error(DiagMessage(config_value->value->GetSource()) - << "resource previously defined here"); + diag->Error(DiagMessage(source) << "duplicate value for resource '" << name << "' " + << "with config '" << config << "'"); + diag->Error(DiagMessage(source) << "resource previously defined here"); return false; case CollisionResult::kKeepOriginal: @@ -441,52 +430,56 @@ bool ResourceTable::AddResourceImpl(const ResourceNameRef& name, const ResourceI return true; } -bool ResourceTable::SetSymbolState(const ResourceNameRef& name, const ResourceId& res_id, - const Symbol& symbol, IDiagnostics* diag) { - return SetSymbolStateImpl(name, res_id, symbol, ValidateName, diag); +bool ResourceTable::SetVisibility(const ResourceNameRef& name, const Visibility& visibility, + IDiagnostics* diag) { + return SetVisibilityImpl(name, visibility, ResourceId{}, ResourceNameValidator, diag); } -bool ResourceTable::SetSymbolStateAllowMangled(const ResourceNameRef& name, - const ResourceId& res_id, - const Symbol& symbol, - IDiagnostics* diag) { - return SetSymbolStateImpl(name, res_id, symbol, SkipValidateName, diag); +bool ResourceTable::SetVisibilityMangled(const ResourceNameRef& name, const Visibility& visibility, + IDiagnostics* diag) { + return SetVisibilityImpl(name, visibility, ResourceId{}, SkipNameValidator, diag); } -bool ResourceTable::SetSymbolStateImpl(const ResourceNameRef& name, const ResourceId& res_id, - const Symbol& symbol, NameValidator name_validator, - IDiagnostics* diag) { +bool ResourceTable::SetVisibilityWithId(const ResourceNameRef& name, const Visibility& visibility, + const ResourceId& res_id, IDiagnostics* diag) { + return SetVisibilityImpl(name, visibility, res_id, ResourceNameValidator, diag); +} + +bool ResourceTable::SetVisibilityWithIdMangled(const ResourceNameRef& name, + const Visibility& visibility, + const ResourceId& res_id, IDiagnostics* diag) { + return SetVisibilityImpl(name, visibility, res_id, SkipNameValidator, diag); +} + +bool ResourceTable::SetVisibilityImpl(const ResourceNameRef& name, const Visibility& visibility, + const ResourceId& res_id, NameValidator name_validator, + IDiagnostics* diag) { CHECK(diag != nullptr); - const StringPiece bad_char = name_validator(name.entry); - if (!bad_char.empty()) { - diag->Error(DiagMessage(symbol.source) << "resource '" << name << "' has invalid entry name '" - << name.entry << "'. Invalid character '" << bad_char - << "'"); + const Source& source = visibility.source; + if (!ValidateName(name_validator, name, source, diag)) { return false; } ResourceTablePackage* package = FindOrCreatePackage(name.package); if (res_id.is_valid_dynamic() && package->id && package->id.value() != res_id.package_id()) { - diag->Error(DiagMessage(symbol.source) - << "trying to add resource '" << name << "' with ID " << res_id - << " but package '" << package->name << "' already has ID " - << std::hex << (int)package->id.value() << std::dec); + diag->Error(DiagMessage(source) << "trying to add resource '" << name << "' with ID " << res_id + << " but package '" << package->name << "' already has ID " + << StringPrintf("%02x", package->id.value())); return false; } ResourceTableType* type = package->FindOrCreateType(name.type); if (res_id.is_valid_dynamic() && type->id && type->id.value() != res_id.type_id()) { - diag->Error(DiagMessage(symbol.source) - << "trying to add resource '" << name << "' with ID " << res_id - << " but type '" << type->type << "' already has ID " - << std::hex << (int)type->id.value() << std::dec); + diag->Error(DiagMessage(source) + << "trying to add resource '" << name << "' with ID " << res_id << " but type '" + << type->type << "' already has ID " << StringPrintf("%02x", type->id.value())); return false; } ResourceEntry* entry = type->FindOrCreateEntry(name.entry); if (res_id.is_valid_dynamic() && entry->id && entry->id.value() != res_id.entry_id()) { - diag->Error(DiagMessage(symbol.source) + diag->Error(DiagMessage(source) << "trying to add resource '" << name << "' with ID " << res_id << " but resource already has ID " << ResourceId(package->id.value(), type->id.value(), entry->id.value())); @@ -499,48 +492,96 @@ bool ResourceTable::SetSymbolStateImpl(const ResourceNameRef& name, const Resour entry->id = res_id.entry_id(); } - // Only mark the type state as public, it doesn't care about being private. - if (symbol.state == SymbolState::kPublic) { - type->symbol_status.state = SymbolState::kPublic; + // Only mark the type visibility level as public, it doesn't care about being private. + if (visibility.level == Visibility::Level::kPublic) { + type->visibility_level = Visibility::Level::kPublic; } - if (symbol.allow_new) { - // This symbol can be added as a new resource when merging (if it belongs to an overlay). - entry->symbol_status.allow_new = true; - } - - if (symbol.state == SymbolState::kUndefined && - entry->symbol_status.state != SymbolState::kUndefined) { + if (visibility.level == Visibility::Level::kUndefined && + entry->visibility.level != Visibility::Level::kUndefined) { // We can't undefine a symbol (remove its visibility). Ignore. return true; } - if (symbol.state == SymbolState::kPrivate && - entry->symbol_status.state == SymbolState::kPublic) { + if (visibility.level < entry->visibility.level) { // We can't downgrade public to private. Ignore. return true; } // This symbol definition takes precedence, replace. - entry->symbol_status.state = symbol.state; - entry->symbol_status.source = symbol.source; - entry->symbol_status.comment = symbol.comment; + entry->visibility = visibility; + return true; +} + +bool ResourceTable::SetAllowNew(const ResourceNameRef& name, const AllowNew& allow_new, + IDiagnostics* diag) { + return SetAllowNewImpl(name, allow_new, ResourceNameValidator, diag); +} + +bool ResourceTable::SetAllowNewMangled(const ResourceNameRef& name, const AllowNew& allow_new, + IDiagnostics* diag) { + return SetAllowNewImpl(name, allow_new, SkipNameValidator, diag); +} + +bool ResourceTable::SetAllowNewImpl(const ResourceNameRef& name, const AllowNew& allow_new, + NameValidator name_validator, IDiagnostics* diag) { + CHECK(diag != nullptr); + + if (!ValidateName(name_validator, name, allow_new.source, diag)) { + return false; + } + + ResourceTablePackage* package = FindOrCreatePackage(name.package); + ResourceTableType* type = package->FindOrCreateType(name.type); + ResourceEntry* entry = type->FindOrCreateEntry(name.entry); + entry->allow_new = allow_new; return true; } -Maybe<ResourceTable::SearchResult> ResourceTable::FindResource(const ResourceNameRef& name) { +bool ResourceTable::SetOverlayable(const ResourceNameRef& name, const Overlayable& overlayable, + IDiagnostics* diag) { + return SetOverlayableImpl(name, overlayable, ResourceNameValidator, diag); +} + +bool ResourceTable::SetOverlayableMangled(const ResourceNameRef& name, + const Overlayable& overlayable, IDiagnostics* diag) { + return SetOverlayableImpl(name, overlayable, SkipNameValidator, diag); +} + +bool ResourceTable::SetOverlayableImpl(const ResourceNameRef& name, const Overlayable& overlayable, + NameValidator name_validator, IDiagnostics* diag) { + CHECK(diag != nullptr); + + if (!ValidateName(name_validator, name, overlayable.source, diag)) { + return false; + } + + ResourceTablePackage* package = FindOrCreatePackage(name.package); + ResourceTableType* type = package->FindOrCreateType(name.type); + ResourceEntry* entry = type->FindOrCreateEntry(name.entry); + if (entry->overlayable) { + diag->Error(DiagMessage(overlayable.source) + << "duplicate overlayable declaration for resource '" << name << "'"); + diag->Error(DiagMessage(entry->overlayable.value().source) << "previous declaration here"); + return false; + } + entry->overlayable = overlayable; + return true; +} + +Maybe<ResourceTable::SearchResult> ResourceTable::FindResource(const ResourceNameRef& name) const { ResourceTablePackage* package = FindPackage(name.package); - if (!package) { + if (package == nullptr) { return {}; } ResourceTableType* type = package->FindType(name.type); - if (!type) { + if (type == nullptr) { return {}; } ResourceEntry* entry = type->FindEntry(name.entry); - if (!entry) { + if (entry == nullptr) { return {}; } return SearchResult{package, type, entry}; @@ -552,23 +593,20 @@ std::unique_ptr<ResourceTable> ResourceTable::Clone() const { ResourceTablePackage* new_pkg = new_table->CreatePackage(pkg->name, pkg->id); for (const auto& type : pkg->types) { ResourceTableType* new_type = new_pkg->FindOrCreateType(type->type); - if (!new_type->id) { - new_type->id = type->id; - new_type->symbol_status = type->symbol_status; - } + new_type->id = type->id; + new_type->visibility_level = type->visibility_level; for (const auto& entry : type->entries) { ResourceEntry* new_entry = new_type->FindOrCreateEntry(entry->name); - if (!new_entry->id) { - new_entry->id = entry->id; - new_entry->symbol_status = entry->symbol_status; - } + new_entry->id = entry->id; + new_entry->visibility = entry->visibility; + new_entry->allow_new = entry->allow_new; + new_entry->overlayable = entry->overlayable; for (const auto& config_value : entry->values) { ResourceConfigValue* new_value = new_entry->FindOrCreateValue(config_value->config, config_value->product); - Value* value = config_value->value->Clone(&new_table->string_pool); - new_value->value = std::unique_ptr<Value>(value); + new_value->value.reset(config_value->value->Clone(&new_table->string_pool)); } } } diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index d5db67e77f51..eaa2d0b8af7d 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -38,40 +38,40 @@ namespace aapt { -enum class SymbolState { - kUndefined, - kPrivate, - kPublic, -}; +// The Public status of a resource. +struct Visibility { + enum class Level { + kUndefined, + kPrivate, + kPublic, + }; -/** - * The Public status of a resource. - */ -struct Symbol { - SymbolState state = SymbolState::kUndefined; + Level level = Level::kUndefined; Source source; + std::string comment; +}; - // Whether this entry (originating from an overlay) can be added as a new resource. - bool allow_new = false; +// Represents <add-resource> in an overlay. +struct AllowNew { + Source source; + std::string comment; +}; +// The policy dictating whether an entry is overlayable at runtime by RROs. +struct Overlayable { + Source source; std::string comment; }; class ResourceConfigValue { public: - /** - * The configuration for which this value is defined. - */ + // The configuration for which this value is defined. const ConfigDescription config; - /** - * The product for which this value is defined. - */ + // The product for which this value is defined. const std::string product; - /** - * The actual Value. - */ + // The actual Value. std::unique_ptr<Value> value; ResourceConfigValue(const ConfigDescription& config, const android::StringPiece& product) @@ -87,27 +87,21 @@ class ResourceConfigValue { */ class ResourceEntry { public: - /** - * The name of the resource. Immutable, as - * this determines the order of this resource - * when doing lookups. - */ + // The name of the resource. Immutable, as this determines the order of this resource + // when doing lookups. const std::string name; - /** - * The entry ID for this resource. - */ + // The entry ID for this resource (the EEEE in 0xPPTTEEEE). Maybe<uint16_t> id; - /** - * Whether this resource is public (and must maintain the same entry ID across - * builds). - */ - Symbol symbol_status; + // Whether this resource is public (and must maintain the same entry ID across builds). + Visibility visibility; + + Maybe<AllowNew> allow_new; - /** - * The resource's values for each configuration. - */ + Maybe<Overlayable> overlayable; + + // The resource's values for each configuration. std::vector<std::unique_ptr<ResourceConfigValue>> values; explicit ResourceEntry(const android::StringPiece& name) : name(name.to_string()) {} @@ -125,31 +119,19 @@ class ResourceEntry { DISALLOW_COPY_AND_ASSIGN(ResourceEntry); }; -/** - * Represents a resource type, which holds entries defined - * for this type. - */ +// Represents a resource type (eg. string, drawable, layout, etc.) containing resource entries. class ResourceTableType { public: - /** - * The logical type of resource (string, drawable, layout, etc.). - */ + // The logical type of resource (string, drawable, layout, etc.). const ResourceType type; - /** - * The type ID for this resource. - */ + // The type ID for this resource (the TT in 0xPPTTEEEE). Maybe<uint8_t> id; - /** - * Whether this type is public (and must maintain the same - * type ID across builds). - */ - Symbol symbol_status; + // Whether this type is public (and must maintain the same type ID across builds). + Visibility::Level visibility_level = Visibility::Level::kUndefined; - /** - * List of resources for this type. - */ + // List of resources for this type. std::vector<std::unique_ptr<ResourceEntry>> entries; explicit ResourceTableType(const ResourceType type) : type(type) {} @@ -163,9 +145,11 @@ class ResourceTableType { class ResourceTablePackage { public: - Maybe<uint8_t> id; std::string name; + // The package ID (the PP in 0xPPTTEEEE). + Maybe<uint8_t> id; + std::vector<std::unique_ptr<ResourceTableType>> types; ResourceTablePackage() = default; @@ -176,10 +160,7 @@ class ResourceTablePackage { DISALLOW_COPY_AND_ASSIGN(ResourceTablePackage); }; -/** - * The container and index for all resources defined for an app. This gets - * flattened into a binary resource table (resources.arsc). - */ +// The container and index for all resources defined for an app. class ResourceTable { public: ResourceTable() = default; @@ -188,47 +169,51 @@ class ResourceTable { using CollisionResolverFunc = std::function<CollisionResult(Value*, Value*)>; - /** - * When a collision of resources occurs, this method decides which value to - * keep. - */ + // When a collision of resources occurs, this method decides which value to keep. static CollisionResult ResolveValueCollision(Value* existing, Value* incoming); bool AddResource(const ResourceNameRef& name, const ConfigDescription& config, const android::StringPiece& product, std::unique_ptr<Value> value, IDiagnostics* diag); - bool AddResource(const ResourceNameRef& name, const ResourceId& res_id, - const ConfigDescription& config, const android::StringPiece& product, - std::unique_ptr<Value> value, IDiagnostics* diag); + bool AddResourceWithId(const ResourceNameRef& name, const ResourceId& res_id, + const ConfigDescription& config, const android::StringPiece& product, + std::unique_ptr<Value> value, IDiagnostics* diag); bool AddFileReference(const ResourceNameRef& name, const ConfigDescription& config, const Source& source, const android::StringPiece& path, IDiagnostics* diag); - bool AddFileReferenceAllowMangled(const ResourceNameRef& name, const ConfigDescription& config, - const Source& source, const android::StringPiece& path, - io::IFile* file, IDiagnostics* diag); - - /** - * Same as AddResource, but doesn't verify the validity of the name. This is - * used - * when loading resources from an existing binary resource table that may have - * mangled - * names. - */ - bool AddResourceAllowMangled(const ResourceNameRef& name, const ConfigDescription& config, - const android::StringPiece& product, std::unique_ptr<Value> value, - IDiagnostics* diag); - - bool AddResourceAllowMangled(const ResourceNameRef& name, const ResourceId& id, - const ConfigDescription& config, const android::StringPiece& product, - std::unique_ptr<Value> value, IDiagnostics* diag); - - bool SetSymbolState(const ResourceNameRef& name, const ResourceId& res_id, - const Symbol& symbol, IDiagnostics* diag); - - bool SetSymbolStateAllowMangled(const ResourceNameRef& name, const ResourceId& res_id, - const Symbol& symbol, IDiagnostics* diag); + bool AddFileReferenceMangled(const ResourceNameRef& name, const ConfigDescription& config, + const Source& source, const android::StringPiece& path, + io::IFile* file, IDiagnostics* diag); + + // Same as AddResource, but doesn't verify the validity of the name. This is used + // when loading resources from an existing binary resource table that may have mangled names. + bool AddResourceMangled(const ResourceNameRef& name, const ConfigDescription& config, + const android::StringPiece& product, std::unique_ptr<Value> value, + IDiagnostics* diag); + + bool AddResourceWithIdMangled(const ResourceNameRef& name, const ResourceId& id, + const ConfigDescription& config, + const android::StringPiece& product, std::unique_ptr<Value> value, + IDiagnostics* diag); + + bool SetVisibility(const ResourceNameRef& name, const Visibility& visibility, IDiagnostics* diag); + bool SetVisibilityMangled(const ResourceNameRef& name, const Visibility& visibility, + IDiagnostics* diag); + bool SetVisibilityWithId(const ResourceNameRef& name, const Visibility& visibility, + const ResourceId& res_id, IDiagnostics* diag); + bool SetVisibilityWithIdMangled(const ResourceNameRef& name, const Visibility& visibility, + const ResourceId& res_id, IDiagnostics* diag); + + bool SetOverlayable(const ResourceNameRef& name, const Overlayable& overlayable, + IDiagnostics* diag); + bool SetOverlayableMangled(const ResourceNameRef& name, const Overlayable& overlayable, + IDiagnostics* diag); + + bool SetAllowNew(const ResourceNameRef& name, const AllowNew& allow_new, IDiagnostics* diag); + bool SetAllowNewMangled(const ResourceNameRef& name, const AllowNew& allow_new, + IDiagnostics* diag); struct SearchResult { ResourceTablePackage* package; @@ -236,40 +221,28 @@ class ResourceTable { ResourceEntry* entry; }; - Maybe<SearchResult> FindResource(const ResourceNameRef& name); + Maybe<SearchResult> FindResource(const ResourceNameRef& name) const; - /** - * Returns the package struct with the given name, or nullptr if such a - * package does not - * exist. The empty string is a valid package and typically is used to - * represent the - * 'current' package before it is known to the ResourceTable. - */ - ResourceTablePackage* FindPackage(const android::StringPiece& name); + // Returns the package struct with the given name, or nullptr if such a package does not + // exist. The empty string is a valid package and typically is used to represent the + // 'current' package before it is known to the ResourceTable. + ResourceTablePackage* FindPackage(const android::StringPiece& name) const; - ResourceTablePackage* FindPackageById(uint8_t id); + ResourceTablePackage* FindPackageById(uint8_t id) const; ResourceTablePackage* CreatePackage(const android::StringPiece& name, Maybe<uint8_t> id = {}); std::unique_ptr<ResourceTable> Clone() const; - /** - * The string pool used by this resource table. Values that reference strings - * must use - * this pool to create their strings. - * - * NOTE: `string_pool` must come before `packages` so that it is destroyed - * after. - * When `string_pool` references are destroyed (as they will be when - * `packages` - * is destroyed), they decrement a refCount, which would cause invalid - * memory access if the pool was already destroyed. - */ + // The string pool used by this resource table. Values that reference strings must use + // this pool to create their strings. + // NOTE: `string_pool` must come before `packages` so that it is destroyed after. + // When `string_pool` references are destroyed (as they will be when `packages` is destroyed), + // they decrement a refCount, which would cause invalid memory access if the pool was already + // destroyed. StringPool string_pool; - /** - * The list of packages in this table, sorted alphabetically by package name. - */ + // The list of packages in this table, sorted alphabetically by package name. std::vector<std::unique_ptr<ResourceTablePackage>> packages; // Set of dynamic packages that this table may reference. Their package names get encoded @@ -284,6 +257,9 @@ class ResourceTable { ResourceTablePackage* FindOrCreatePackage(const android::StringPiece& name); + bool ValidateName(NameValidator validator, const ResourceNameRef& name, const Source& source, + IDiagnostics* diag); + bool AddResourceImpl(const ResourceNameRef& name, const ResourceId& res_id, const ConfigDescription& config, const android::StringPiece& product, std::unique_ptr<Value> value, NameValidator name_validator, @@ -293,8 +269,19 @@ class ResourceTable { const Source& source, const android::StringPiece& path, io::IFile* file, NameValidator name_validator, IDiagnostics* diag); + bool SetVisibilityImpl(const ResourceNameRef& name, const Visibility& visibility, + const ResourceId& res_id, NameValidator name_validator, + IDiagnostics* diag); + + bool SetAllowNewImpl(const ResourceNameRef& name, const AllowNew& allow_new, + NameValidator name_validator, IDiagnostics* diag); + + bool SetOverlayableImpl(const ResourceNameRef& name, const Overlayable& overlayable, + NameValidator name_validator, IDiagnostics* diag); + bool SetSymbolStateImpl(const ResourceNameRef& name, const ResourceId& res_id, - const Symbol& symbol, NameValidator name_validator, IDiagnostics* diag); + const Visibility& symbol, NameValidator name_validator, + IDiagnostics* diag); DISALLOW_COPY_AND_ASSIGN(ResourceTable); }; diff --git a/tools/aapt2/ResourceTable_test.cpp b/tools/aapt2/ResourceTable_test.cpp index 2a3c131f7b4b..eb75f947e0be 100644 --- a/tools/aapt2/ResourceTable_test.cpp +++ b/tools/aapt2/ResourceTable_test.cpp @@ -24,7 +24,10 @@ #include <ostream> #include <string> +using ::android::StringPiece; +using ::testing::Eq; using ::testing::NotNull; +using ::testing::StrEq; namespace aapt { @@ -45,7 +48,7 @@ TEST(ResourceTableTest, FailToAddResourceWithBadName) { TEST(ResourceTableTest, AddResourceWithWeirdNameWhenAddingMangledResources) { ResourceTable table; - EXPECT_TRUE(table.AddResourceAllowMangled( + EXPECT_TRUE(table.AddResourceMangled( test::ParseNameOrDie("android:id/heythere "), ConfigDescription{}, "", test::ValueBuilder<Id>().SetSource("test.xml", 21u).Build(), test::GetDiagnostics())); } @@ -141,4 +144,104 @@ TEST(ResourceTableTest, ProductVaryingValues) { EXPECT_EQ(std::string("tablet"), values[1]->product); } +static StringPiece LevelToString(Visibility::Level level) { + switch (level) { + case Visibility::Level::kPrivate: + return "private"; + case Visibility::Level::kPublic: + return "private"; + default: + return "undefined"; + } +} + +static ::testing::AssertionResult VisibilityOfResource(const ResourceTable& table, + const ResourceNameRef& name, + Visibility::Level level, + const StringPiece& comment) { + Maybe<ResourceTable::SearchResult> result = table.FindResource(name); + if (!result) { + return ::testing::AssertionFailure() << "no resource '" << name << "' found in table"; + } + + const Visibility& visibility = result.value().entry->visibility; + if (visibility.level != level) { + return ::testing::AssertionFailure() << "expected visibility " << LevelToString(level) + << " but got " << LevelToString(visibility.level); + } + + if (visibility.comment != comment) { + return ::testing::AssertionFailure() << "expected visibility comment '" << comment + << "' but got '" << visibility.comment << "'"; + } + return ::testing::AssertionSuccess(); +} + +TEST(ResourceTableTest, SetVisibility) { + using Level = Visibility::Level; + + ResourceTable table; + const ResourceName name = test::ParseNameOrDie("android:string/foo"); + + Visibility visibility; + visibility.level = Visibility::Level::kPrivate; + visibility.comment = "private"; + ASSERT_TRUE(table.SetVisibility(name, visibility, test::GetDiagnostics())); + ASSERT_TRUE(VisibilityOfResource(table, name, Level::kPrivate, "private")); + + visibility.level = Visibility::Level::kUndefined; + visibility.comment = "undefined"; + ASSERT_TRUE(table.SetVisibility(name, visibility, test::GetDiagnostics())); + ASSERT_TRUE(VisibilityOfResource(table, name, Level::kPrivate, "private")); + + visibility.level = Visibility::Level::kPublic; + visibility.comment = "public"; + ASSERT_TRUE(table.SetVisibility(name, visibility, test::GetDiagnostics())); + ASSERT_TRUE(VisibilityOfResource(table, name, Level::kPublic, "public")); + + visibility.level = Visibility::Level::kPrivate; + visibility.comment = "private"; + ASSERT_TRUE(table.SetVisibility(name, visibility, test::GetDiagnostics())); + ASSERT_TRUE(VisibilityOfResource(table, name, Level::kPublic, "public")); +} + +TEST(ResourceTableTest, SetAllowNew) { + ResourceTable table; + const ResourceName name = test::ParseNameOrDie("android:string/foo"); + + AllowNew allow_new; + Maybe<ResourceTable::SearchResult> result; + + allow_new.comment = "first"; + ASSERT_TRUE(table.SetAllowNew(name, allow_new, test::GetDiagnostics())); + result = table.FindResource(name); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value().entry->allow_new); + ASSERT_THAT(result.value().entry->allow_new.value().comment, StrEq("first")); + + allow_new.comment = "second"; + ASSERT_TRUE(table.SetAllowNew(name, allow_new, test::GetDiagnostics())); + result = table.FindResource(name); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value().entry->allow_new); + ASSERT_THAT(result.value().entry->allow_new.value().comment, StrEq("second")); +} + +TEST(ResourceTableTest, SetOverlayable) { + ResourceTable table; + const ResourceName name = test::ParseNameOrDie("android:string/foo"); + + Overlayable overlayable; + + overlayable.comment = "first"; + ASSERT_TRUE(table.SetOverlayable(name, overlayable, test::GetDiagnostics())); + Maybe<ResourceTable::SearchResult> result = table.FindResource(name); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value().entry->overlayable); + ASSERT_THAT(result.value().entry->overlayable.value().comment, StrEq("first")); + + overlayable.comment = "second"; + ASSERT_FALSE(table.SetOverlayable(name, overlayable, test::GetDiagnostics())); +} + } // namespace aapt diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto index 7e7c86d53d24..8552195d338d 100644 --- a/tools/aapt2/Resources.proto +++ b/tools/aapt2/Resources.proto @@ -93,11 +93,10 @@ message Type { repeated Entry entry = 3; } -// The status of a symbol/entry. This contains information like visibility (public/private), -// comments, and whether the entry can be overridden. -message SymbolStatus { +// The Visibility of a symbol/entry (public, private, undefined). +message Visibility { // The visibility of the resource outside of its package. - enum Visibility { + enum Level { // No visibility was explicitly specified. This is typically treated as private. // The distinction is important when two separate R.java files are generated: a public and // private one. An unknown visibility, in this case, would cause the resource to be omitted @@ -115,17 +114,32 @@ message SymbolStatus { PUBLIC = 2; } - Visibility visibility = 1; + Level level = 1; // The path at which this entry's visibility was defined (eg. public.xml). Source source = 2; // The comment associated with the <public> tag. string comment = 3; +} + +// Whether a resource comes from a compile-time overlay and is explicitly allowed to not overlay an +// existing resource. +message AllowNew { + // Where this was defined in source. + Source source = 1; - // Whether the symbol can be merged into another resource table without there being an existing - // definition to override. Used for overlays and set to true when <add-resource> is specified. - bool allow_new = 4; + // Any comment associated with the declaration. + string comment = 2; +} + +// Whether a resource is overlayable by runtime resource overlays (RRO). +message Overlayable { + // Where this declaration was defined in source. + Source source = 1; + + // Any comment associated with the declaration. + string comment = 2; } // An entry ID in the range [0x0000, 0xffff]. @@ -147,12 +161,19 @@ message Entry { // form package:type/entry. string name = 2; - // The symbol status of this entry, which includes visibility information. - SymbolStatus symbol_status = 3; + // The visibility of this entry (public, private, undefined). + Visibility visibility = 3; + + // Whether this resource, when originating from a compile-time overlay, is allowed to NOT overlay + // any existing resources. + AllowNew allow_new = 4; + + // Whether this resource can be overlaid by a runtime resource overlay (RRO). + Overlayable overlayable = 5; // The set of values defined for this entry, each corresponding to a different // configuration/variant. - repeated ConfigValue config_value = 4; + repeated ConfigValue config_value = 6; } // A Configuration/Value pair. diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index 83512b9126da..7c1e96e88fee 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -49,6 +49,7 @@ #include "xml/XmlPullParser.h" using ::aapt::io::FileInputStream; +using ::aapt::text::Printer; using ::android::StringPiece; using ::android::base::SystemErrorCodeToString; using ::google::protobuf::io::CopyingOutputStreamAdaptor; @@ -112,6 +113,7 @@ static Maybe<ResourcePathData> ExtractResourcePathData(const std::string& path, struct CompileOptions { std::string output_path; Maybe<std::string> res_dir; + Maybe<std::string> generate_text_symbols_path; bool pseudolocalize = false; bool no_png_crunch = false; bool legacy_mode = false; @@ -261,6 +263,58 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to finish entry"); return false; } + + if (options.generate_text_symbols_path) { + io::FileOutputStream fout_text(options.generate_text_symbols_path.value()); + + if (fout_text.HadError()) { + context->GetDiagnostics()->Error(DiagMessage() + << "failed writing to'" + << options.generate_text_symbols_path.value() + << "': " << fout_text.GetError()); + return false; + } + + Printer r_txt_printer(&fout_text); + for (const auto& package : table.packages) { + for (const auto& type : package->types) { + for (const auto& entry : type->entries) { + // Check access modifiers. + switch(entry->visibility.level) { + case Visibility::Level::kUndefined : + r_txt_printer.Print("default "); + break; + case Visibility::Level::kPublic : + r_txt_printer.Print("public "); + break; + case Visibility::Level::kPrivate : + r_txt_printer.Print("private "); + } + + if (type->type != ResourceType::kStyleable) { + r_txt_printer.Print("int "); + r_txt_printer.Print(to_string(type->type)); + r_txt_printer.Print(" "); + r_txt_printer.Println(entry->name); + } else { + r_txt_printer.Print("int[] styleable "); + r_txt_printer.Println(entry->name); + + if (!entry->values.empty()) { + auto styleable = static_cast<const Styleable*>(entry->values.front()->value.get()); + for (const auto& attr : styleable->entries) { + r_txt_printer.Print("default int styleable "); + r_txt_printer.Print(entry->name); + r_txt_printer.Print("_"); + r_txt_printer.Println(attr.name.value().entry); + } + } + } + } + } + } + } + return true; } @@ -402,6 +456,31 @@ static bool CompileXml(IAaptContext* context, const CompileOptions& options, context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to finish writing data"); return false; } + + if (options.generate_text_symbols_path) { + io::FileOutputStream fout_text(options.generate_text_symbols_path.value()); + + if (fout_text.HadError()) { + context->GetDiagnostics()->Error(DiagMessage() + << "failed writing to'" + << options.generate_text_symbols_path.value() + << "': " << fout_text.GetError()); + return false; + } + + Printer r_txt_printer(&fout_text); + for (const auto res : xmlres->file.exported_symbols) { + r_txt_printer.Print("default int id "); + r_txt_printer.Println(res.name.entry); + } + + // And print ourselves. + r_txt_printer.Print("default int "); + r_txt_printer.Print(path_data.resource_dir); + r_txt_printer.Print(" "); + r_txt_printer.Println(path_data.name); + } + return true; } @@ -609,6 +688,10 @@ int Compile(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { Flags() .RequiredFlag("-o", "Output path", &options.output_path) .OptionalFlag("--dir", "Directory to scan for resources", &options.res_dir) + .OptionalFlag("--output-text-symbols", + "Generates a text file containing the resource symbols in the\n" + "specified file", + &options.generate_text_symbols_path) .OptionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales " "(en-XA and ar-XB)", diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp index fc1f1d6342ae..12113ed8a48a 100644 --- a/tools/aapt2/cmd/Diff.cpp +++ b/tools/aapt2/cmd/Diff.cpp @@ -75,14 +75,14 @@ static void EmitDiffLine(const Source& source, const StringPiece& message) { std::cerr << source << ": " << message << "\n"; } -static bool IsSymbolVisibilityDifferent(const Symbol& symbol_a, const Symbol& symbol_b) { - return symbol_a.state != symbol_b.state; +static bool IsSymbolVisibilityDifferent(const Visibility& vis_a, const Visibility& vis_b) { + return vis_a.level != vis_b.level; } template <typename Id> -static bool IsIdDiff(const Symbol& symbol_a, const Maybe<Id>& id_a, const Symbol& symbol_b, - const Maybe<Id>& id_b) { - if (symbol_a.state == SymbolState::kPublic || symbol_b.state == SymbolState::kPublic) { +static bool IsIdDiff(const Visibility::Level& level_a, const Maybe<Id>& id_a, + const Visibility::Level& level_b, const Maybe<Id>& id_b) { + if (level_a == Visibility::Level::kPublic || level_b == Visibility::Level::kPublic) { return id_a != id_b; } return false; @@ -157,17 +157,17 @@ static bool EmitResourceTypeDiff(IAaptContext* context, LoadedApk* apk_a, EmitDiffLine(apk_b->GetSource(), str_stream.str()); diff = true; } else { - if (IsSymbolVisibilityDifferent(entry_a->symbol_status, entry_b->symbol_status)) { + if (IsSymbolVisibilityDifferent(entry_a->visibility, entry_b->visibility)) { std::stringstream str_stream; str_stream << pkg_a->name << ":" << type_a->type << "/" << entry_a->name << " has different visibility ("; - if (entry_b->symbol_status.state == SymbolState::kPublic) { + if (entry_b->visibility.level == Visibility::Level::kPublic) { str_stream << "PUBLIC"; } else { str_stream << "PRIVATE"; } str_stream << " vs "; - if (entry_a->symbol_status.state == SymbolState::kPublic) { + if (entry_a->visibility.level == Visibility::Level::kPublic) { str_stream << "PUBLIC"; } else { str_stream << "PRIVATE"; @@ -175,7 +175,7 @@ static bool EmitResourceTypeDiff(IAaptContext* context, LoadedApk* apk_a, str_stream << ")"; EmitDiffLine(apk_b->GetSource(), str_stream.str()); diff = true; - } else if (IsIdDiff(entry_a->symbol_status, entry_a->id, entry_b->symbol_status, + } else if (IsIdDiff(entry_a->visibility.level, entry_a->id, entry_b->visibility.level, entry_b->id)) { std::stringstream str_stream; str_stream << pkg_a->name << ":" << type_a->type << "/" << entry_a->name @@ -225,16 +225,16 @@ static bool EmitResourcePackageDiff(IAaptContext* context, LoadedApk* apk_a, EmitDiffLine(apk_a->GetSource(), str_stream.str()); diff = true; } else { - if (IsSymbolVisibilityDifferent(type_a->symbol_status, type_b->symbol_status)) { + if (type_a->visibility_level != type_b->visibility_level) { std::stringstream str_stream; str_stream << pkg_a->name << ":" << type_a->type << " has different visibility ("; - if (type_b->symbol_status.state == SymbolState::kPublic) { + if (type_b->visibility_level == Visibility::Level::kPublic) { str_stream << "PUBLIC"; } else { str_stream << "PRIVATE"; } str_stream << " vs "; - if (type_a->symbol_status.state == SymbolState::kPublic) { + if (type_a->visibility_level == Visibility::Level::kPublic) { str_stream << "PUBLIC"; } else { str_stream << "PRIVATE"; @@ -242,7 +242,8 @@ static bool EmitResourcePackageDiff(IAaptContext* context, LoadedApk* apk_a, str_stream << ")"; EmitDiffLine(apk_b->GetSource(), str_stream.str()); diff = true; - } else if (IsIdDiff(type_a->symbol_status, type_a->id, type_b->symbol_status, type_b->id)) { + } else if (IsIdDiff(type_a->visibility_level, type_a->id, type_b->visibility_level, + type_b->id)) { std::stringstream str_stream; str_stream << pkg_a->name << ":" << type_a->type << " has different public ID ("; if (type_b->id) { diff --git a/tools/aapt2/cmd/Dump.cpp b/tools/aapt2/cmd/Dump.cpp index bc7f5a86b043..3d2fb556cf59 100644 --- a/tools/aapt2/cmd/Dump.cpp +++ b/tools/aapt2/cmd/Dump.cpp @@ -69,15 +69,13 @@ static void DumpCompiledFile(const ResourceFile& file, const Source& source, off printer->Println(StringPrintf("Data: offset=%" PRIi64 " length=%zd", offset, len)); } -static bool TryDumpFile(IAaptContext* context, const std::string& file_path) { +static bool TryDumpFile(IAaptContext* context, const std::string& file_path, + const DebugPrintTableOptions& print_options) { // Use a smaller buffer so that there is less latency for dumping to stdout. constexpr size_t kStdOutBufferSize = 1024u; io::FileOutputStream fout(STDOUT_FILENO, kStdOutBufferSize); Printer printer(&fout); - DebugPrintTableOptions print_options; - print_options.show_sources = true; - std::string err; std::unique_ptr<io::ZipFileCollection> zip = io::ZipFileCollection::Create(file_path, &err); if (zip) { @@ -244,7 +242,12 @@ class DumpContext : public IAaptContext { // Entry point for dump command. int Dump(const std::vector<StringPiece>& args) { bool verbose = false; - Flags flags = Flags().OptionalSwitch("-v", "increase verbosity of output", &verbose); + bool no_values = false; + Flags flags = Flags() + .OptionalSwitch("--no-values", + "Suppresses output of values when displaying resource tables.", + &no_values) + .OptionalSwitch("-v", "increase verbosity of output", &verbose); if (!flags.Parse("aapt2 dump", args, &std::cerr)) { return 1; } @@ -252,8 +255,11 @@ int Dump(const std::vector<StringPiece>& args) { DumpContext context; context.SetVerbose(verbose); + DebugPrintTableOptions dump_table_options; + dump_table_options.show_sources = true; + dump_table_options.show_values = !no_values; for (const std::string& arg : flags.GetArgs()) { - if (!TryDumpFile(&context, arg)) { + if (!TryDumpFile(&context, arg, dump_table_options)) { return 1; } } diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index d782de55f66a..72e07dc23725 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -631,9 +631,9 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv dst_path = ResourceUtils::BuildResourceFileName(doc->file, context_->GetNameMangler()); - bool result = table->AddFileReferenceAllowMangled(doc->file.name, doc->file.config, - doc->file.source, dst_path, nullptr, - context_->GetDiagnostics()); + bool result = + table->AddFileReferenceMangled(doc->file.name, doc->file.config, doc->file.source, + dst_path, nullptr, context_->GetDiagnostics()); if (!result) { return false; } @@ -1343,9 +1343,9 @@ class LinkCommand { std::unique_ptr<Id> id = util::make_unique<Id>(); id->SetSource(source.WithLine(exported_symbol.line)); - bool result = final_table_.AddResourceAllowMangled( - res_name, ConfigDescription::DefaultConfig(), std::string(), std::move(id), - context_->GetDiagnostics()); + bool result = + final_table_.AddResourceMangled(res_name, ConfigDescription::DefaultConfig(), + std::string(), std::move(id), context_->GetDiagnostics()); if (!result) { return false; } @@ -2121,6 +2121,9 @@ int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) { &options.manifest_fixer_options.rename_instrumentation_target_package) .OptionalFlagList("-0", "File extensions not to compress.", &options.extensions_to_not_compress) + .OptionalSwitch("--warn-manifest-validation", + "Treat manifest validation errors as warnings.", + &options.manifest_fixer_options.warn_validation) .OptionalFlagList("--split", "Split resources matching a set of configs out to a Split APK.\n" "Syntax: path/to/output.apk:<config>[,<config>[...]].\n" diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index d8bb9992c152..9c76119f9504 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -377,44 +377,10 @@ int Optimize(const std::vector<StringPiece>& args) { } const std::string& apk_path = flags.GetArgs()[0]; - std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(apk_path, context.GetDiagnostics()); - if (!apk) { - return 1; - } context.SetVerbose(verbose); IDiagnostics* diag = context.GetDiagnostics(); - if (target_densities) { - // Parse the target screen densities. - for (const StringPiece& config_str : util::Tokenize(target_densities.value(), ',')) { - Maybe<uint16_t> target_density = ParseTargetDensityParameter(config_str, diag); - if (!target_density) { - return 1; - } - options.table_splitter_options.preferred_densities.push_back(target_density.value()); - } - } - - std::unique_ptr<IConfigFilter> filter; - if (!configs.empty()) { - filter = ParseConfigFilterParameters(configs, diag); - if (filter == nullptr) { - return 1; - } - options.table_splitter_options.config_filter = filter.get(); - } - - // Parse the split parameters. - for (const std::string& split_arg : split_args) { - options.split_paths.emplace_back(); - options.split_constraints.emplace_back(); - if (!ParseSplitParameter(split_arg, diag, &options.split_paths.back(), - &options.split_constraints.back())) { - return 1; - } - } - if (config_path) { std::string& path = config_path.value(); Maybe<ConfigurationParser> for_path = ConfigurationParser::ForPath(path); @@ -456,6 +422,41 @@ int Optimize(const std::vector<StringPiece>& args) { return 1; } + std::unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(apk_path, context.GetDiagnostics()); + if (!apk) { + return 1; + } + + if (target_densities) { + // Parse the target screen densities. + for (const StringPiece& config_str : util::Tokenize(target_densities.value(), ',')) { + Maybe<uint16_t> target_density = ParseTargetDensityParameter(config_str, diag); + if (!target_density) { + return 1; + } + options.table_splitter_options.preferred_densities.push_back(target_density.value()); + } + } + + std::unique_ptr<IConfigFilter> filter; + if (!configs.empty()) { + filter = ParseConfigFilterParameters(configs, diag); + if (filter == nullptr) { + return 1; + } + options.table_splitter_options.config_filter = filter.get(); + } + + // Parse the split parameters. + for (const std::string& split_arg : split_args) { + options.split_paths.emplace_back(); + options.split_constraints.emplace_back(); + if (!ParseSplitParameter(split_arg, diag, &options.split_paths.back(), + &options.split_constraints.back())) { + return 1; + } + } + if (options.table_flattener_options.collapse_key_stringpool) { if (whitelist_path) { std::string& path = whitelist_path.value(); diff --git a/tools/aapt2/configuration/ConfigurationParser.cpp b/tools/aapt2/configuration/ConfigurationParser.cpp index ebc523f096db..eabeb47fccec 100644 --- a/tools/aapt2/configuration/ConfigurationParser.cpp +++ b/tools/aapt2/configuration/ConfigurationParser.cpp @@ -49,13 +49,15 @@ using ::aapt::configuration::AndroidSdk; using ::aapt::configuration::ConfiguredArtifact; using ::aapt::configuration::DeviceFeature; using ::aapt::configuration::Entry; +using ::aapt::configuration::ExtractConfiguration; using ::aapt::configuration::GlTexture; using ::aapt::configuration::Group; using ::aapt::configuration::Locale; +using ::aapt::configuration::OrderedEntry; using ::aapt::configuration::OutputArtifact; using ::aapt::configuration::PostProcessingConfiguration; using ::aapt::configuration::handler::AbiGroupTagHandler; -using ::aapt::configuration::handler::AndroidSdkGroupTagHandler; +using ::aapt::configuration::handler::AndroidSdkTagHandler; using ::aapt::configuration::handler::ArtifactFormatTagHandler; using ::aapt::configuration::handler::ArtifactTagHandler; using ::aapt::configuration::handler::DeviceFeatureGroupTagHandler; @@ -130,7 +132,7 @@ bool CopyXmlReferences(const Maybe<std::string>& name, const Group<T>& groups, return false; } - for (const T& item : group->second) { + for (const T& item : group->second.entry) { target->push_back(item); } return true; @@ -188,61 +190,6 @@ xml::XmlNodeAction::ActionFuncWithDiag Bind(configuration::PostProcessingConfigu }; } -/** Returns the binary reprasentation of the XML configuration. */ -Maybe<PostProcessingConfiguration> ExtractConfiguration(const std::string& contents, - IDiagnostics* diag) { - StringInputStream in(contents); - std::unique_ptr<xml::XmlResource> doc = xml::Inflate(&in, diag, Source("config.xml")); - if (!doc) { - return {}; - } - - // Strip any namespaces from the XML as the XmlActionExecutor ignores anything with a namespace. - Element* root = doc->root.get(); - if (root == nullptr) { - diag->Error(DiagMessage() << "Could not find the root element in the XML document"); - return {}; - } - - std::string& xml_ns = root->namespace_uri; - if (!xml_ns.empty()) { - if (xml_ns != kAaptXmlNs) { - diag->Error(DiagMessage() << "Unknown namespace found on root element: " << xml_ns); - return {}; - } - - xml_ns.clear(); - NamespaceVisitor visitor; - root->Accept(&visitor); - } - - XmlActionExecutor executor; - XmlNodeAction& root_action = executor["post-process"]; - XmlNodeAction& artifacts_action = root_action["artifacts"]; - XmlNodeAction& groups_action = root_action["groups"]; - - PostProcessingConfiguration config; - - // Parse the artifact elements. - artifacts_action["artifact"].Action(Bind(&config, ArtifactTagHandler)); - artifacts_action["artifact-format"].Action(Bind(&config, ArtifactFormatTagHandler)); - - // Parse the different configuration groups. - groups_action["abi-group"].Action(Bind(&config, AbiGroupTagHandler)); - groups_action["screen-density-group"].Action(Bind(&config, ScreenDensityGroupTagHandler)); - groups_action["locale-group"].Action(Bind(&config, LocaleGroupTagHandler)); - groups_action["android-sdk-group"].Action(Bind(&config, AndroidSdkGroupTagHandler)); - groups_action["gl-texture-group"].Action(Bind(&config, GlTextureGroupTagHandler)); - groups_action["device-feature-group"].Action(Bind(&config, DeviceFeatureGroupTagHandler)); - - if (!executor.Execute(XmlActionExecutorPolicy::kNone, diag, doc.get())) { - diag->Error(DiagMessage() << "Could not process XML document"); - return {}; - } - - return {config}; -} - /** Converts a ConfiguredArtifact into an OutputArtifact. */ Maybe<OutputArtifact> ToOutputArtifact(const ConfiguredArtifact& artifact, const std::string& apk_name, @@ -302,11 +249,11 @@ Maybe<OutputArtifact> ToOutputArtifact(const ConfiguredArtifact& artifact, has_errors = true; } - if (artifact.android_sdk_group) { - auto entry = config.android_sdk_groups.find(artifact.android_sdk_group.value()); - if (entry == config.android_sdk_groups.end()) { + if (artifact.android_sdk) { + auto entry = config.android_sdks.find(artifact.android_sdk.value()); + if (entry == config.android_sdks.end()) { src_diag.Error(DiagMessage() << "Could not lookup required Android SDK version: " - << artifact.android_sdk_group.value()); + << artifact.android_sdk.value()); has_errors = true; } else { output_artifact.android_sdk = {entry->second}; @@ -323,6 +270,64 @@ Maybe<OutputArtifact> ToOutputArtifact(const ConfiguredArtifact& artifact, namespace configuration { +/** Returns the binary reprasentation of the XML configuration. */ +Maybe<PostProcessingConfiguration> ExtractConfiguration(const std::string& contents, + const std::string& config_path, + IDiagnostics* diag) { + StringInputStream in(contents); + std::unique_ptr<xml::XmlResource> doc = xml::Inflate(&in, diag, Source(config_path)); + if (!doc) { + return {}; + } + + // Strip any namespaces from the XML as the XmlActionExecutor ignores anything with a namespace. + Element* root = doc->root.get(); + if (root == nullptr) { + diag->Error(DiagMessage() << "Could not find the root element in the XML document"); + return {}; + } + + std::string& xml_ns = root->namespace_uri; + if (!xml_ns.empty()) { + if (xml_ns != kAaptXmlNs) { + diag->Error(DiagMessage() << "Unknown namespace found on root element: " << xml_ns); + return {}; + } + + xml_ns.clear(); + NamespaceVisitor visitor; + root->Accept(&visitor); + } + + XmlActionExecutor executor; + XmlNodeAction& root_action = executor["post-process"]; + XmlNodeAction& artifacts_action = root_action["artifacts"]; + + PostProcessingConfiguration config; + + // Parse the artifact elements. + artifacts_action["artifact"].Action(Bind(&config, ArtifactTagHandler)); + artifacts_action["artifact-format"].Action(Bind(&config, ArtifactFormatTagHandler)); + + // Parse the different configuration groups. + root_action["abi-groups"]["abi-group"].Action(Bind(&config, AbiGroupTagHandler)); + root_action["screen-density-groups"]["screen-density-group"].Action( + Bind(&config, ScreenDensityGroupTagHandler)); + root_action["locale-groups"]["locale-group"].Action(Bind(&config, LocaleGroupTagHandler)); + root_action["android-sdks"]["android-sdk"].Action(Bind(&config, AndroidSdkTagHandler)); + root_action["gl-texture-groups"]["gl-texture-group"].Action( + Bind(&config, GlTextureGroupTagHandler)); + root_action["device-feature-groups"]["device-feature-group"].Action( + Bind(&config, DeviceFeatureGroupTagHandler)); + + if (!executor.Execute(XmlActionExecutorPolicy::kNone, diag, doc.get())) { + diag->Error(DiagMessage() << "Could not process XML document"); + return {}; + } + + return {config}; +} + const StringPiece& AbiToString(Abi abi) { return kAbiToStringMap.at(static_cast<size_t>(abi)); } @@ -383,7 +388,7 @@ Maybe<std::string> ConfiguredArtifact::ToArtifactName(const StringPiece& format, return {}; } - if (!ReplacePlaceholder("${sdk}", android_sdk_group, &result, diag)) { + if (!ReplacePlaceholder("${sdk}", android_sdk, &result, diag)) { return {}; } @@ -414,47 +419,37 @@ Maybe<ConfigurationParser> ConfigurationParser::ForPath(const std::string& path) if (!ReadFileToString(path, &contents, true)) { return {}; } - return ConfigurationParser(contents); + return ConfigurationParser(contents, path); } -ConfigurationParser::ConfigurationParser(std::string contents) - : contents_(std::move(contents)), - diag_(&noop_) { +ConfigurationParser::ConfigurationParser(std::string contents, const std::string& config_path) + : contents_(std::move(contents)), config_path_(config_path), diag_(&noop_) { } Maybe<std::vector<OutputArtifact>> ConfigurationParser::Parse( const android::StringPiece& apk_path) { - Maybe<PostProcessingConfiguration> maybe_config = ExtractConfiguration(contents_, diag_); + Maybe<PostProcessingConfiguration> maybe_config = + ExtractConfiguration(contents_, config_path_, diag_); if (!maybe_config) { return {}; } - const PostProcessingConfiguration& config = maybe_config.value(); - - // TODO: Automatically arrange artifacts so that they match Play Store multi-APK requirements. - // see: https://developer.android.com/google/play/publishing/multiple-apks.html - // - // For now, make sure the version codes are unique. - std::vector<ConfiguredArtifact> artifacts = config.artifacts; - std::sort(artifacts.begin(), artifacts.end()); - if (std::adjacent_find(artifacts.begin(), artifacts.end()) != artifacts.end()) { - diag_->Error(DiagMessage() << "Configuration has duplicate versions"); - return {}; - } - - const std::string& apk_name = file::GetFilename(apk_path).to_string(); - const StringPiece ext = file::GetExtension(apk_name); - const std::string base_name = apk_name.substr(0, apk_name.size() - ext.size()); // Convert from a parsed configuration to a list of artifacts for processing. + const std::string& apk_name = file::GetFilename(apk_path).to_string(); std::vector<OutputArtifact> output_artifacts; bool has_errors = false; - for (const ConfiguredArtifact& artifact : artifacts) { + PostProcessingConfiguration& config = maybe_config.value(); + config.SortArtifacts(); + + int version = 1; + for (const ConfiguredArtifact& artifact : config.artifacts) { Maybe<OutputArtifact> output_artifact = ToOutputArtifact(artifact, apk_name, config, diag_); if (!output_artifact) { // Defer return an error condition so that all errors are reported. has_errors = true; } else { + output_artifact.value().version = version++; output_artifacts.push_back(std::move(output_artifact.value())); } } @@ -470,24 +465,18 @@ namespace handler { bool ArtifactTagHandler(PostProcessingConfiguration* config, Element* root_element, IDiagnostics* diag) { - // This will be incremented later so the first version will always be different to the base APK. - int current_version = (config->artifacts.empty()) ? 0 : config->artifacts.back().version; - ConfiguredArtifact artifact{}; - Maybe<int> version; for (const auto& attr : root_element->attributes) { if (attr.name == "name") { artifact.name = attr.value; - } else if (attr.name == "version") { - version = std::stoi(attr.value); } else if (attr.name == "abi-group") { artifact.abi_group = {attr.value}; } else if (attr.name == "screen-density-group") { artifact.screen_density_group = {attr.value}; } else if (attr.name == "locale-group") { artifact.locale_group = {attr.value}; - } else if (attr.name == "android-sdk-group") { - artifact.android_sdk_group = {attr.value}; + } else if (attr.name == "android-sdk") { + artifact.android_sdk = {attr.value}; } else if (attr.name == "gl-texture-group") { artifact.gl_texture_group = {attr.value}; } else if (attr.name == "device-feature-group") { @@ -497,9 +486,6 @@ bool ArtifactTagHandler(PostProcessingConfiguration* config, Element* root_eleme << attr.value); } } - - artifact.version = (version) ? version.value() : current_version + 1; - config->artifacts.push_back(artifact); return true; }; @@ -523,9 +509,19 @@ bool AbiGroupTagHandler(PostProcessingConfiguration* config, Element* root_eleme return false; } - auto& group = config->abi_groups[label]; + auto& group = GetOrCreateGroup(label, &config->abi_groups); bool valid = true; + // Special case for empty abi-group tag. Label will be used as the ABI. + if (root_element->GetChildElements().empty()) { + auto abi = kStringToAbiMap.find(label); + if (abi == kStringToAbiMap.end()) { + return false; + } + group.push_back(abi->second); + return true; + } + for (auto* child : root_element->GetChildElements()) { if (child->name != "abi") { diag->Error(DiagMessage() << "Unexpected element in ABI group: " << child->name); @@ -534,7 +530,13 @@ bool AbiGroupTagHandler(PostProcessingConfiguration* config, Element* root_eleme for (auto& node : child->children) { xml::Text* t; if ((t = NodeCast<xml::Text>(node.get())) != nullptr) { - group.push_back(kStringToAbiMap.at(TrimWhitespace(t->text).to_string())); + auto abi = kStringToAbiMap.find(TrimWhitespace(t->text).to_string()); + if (abi != kStringToAbiMap.end()) { + group.push_back(abi->second); + } else { + diag->Error(DiagMessage() << "Could not parse ABI value: " << t->text); + valid = false; + } break; } } @@ -551,9 +553,28 @@ bool ScreenDensityGroupTagHandler(PostProcessingConfiguration* config, Element* return false; } - auto& group = config->screen_density_groups[label]; + auto& group = GetOrCreateGroup(label, &config->screen_density_groups); bool valid = true; + // Special case for empty screen-density-group tag. Label will be used as the screen density. + if (root_element->GetChildElements().empty()) { + ConfigDescription config_descriptor; + bool parsed = ConfigDescription::Parse(label, &config_descriptor); + if (parsed && + (config_descriptor.CopyWithoutSdkVersion().diff(ConfigDescription::DefaultConfig()) == + android::ResTable_config::CONFIG_DENSITY)) { + // Copy the density with the minimum SDK version stripped out. + group.push_back(config_descriptor.CopyWithoutSdkVersion()); + } else { + diag->Error(DiagMessage() + << "Could not parse config descriptor for empty screen-density-group: " + << label); + valid = false; + } + + return valid; + } + for (auto* child : root_element->GetChildElements()) { if (child->name != "screen-density") { diag->Error(DiagMessage() << "Unexpected root_element in screen density group: " @@ -592,9 +613,28 @@ bool LocaleGroupTagHandler(PostProcessingConfiguration* config, Element* root_el return false; } - auto& group = config->locale_groups[label]; + auto& group = GetOrCreateGroup(label, &config->locale_groups); bool valid = true; + // Special case to auto insert a locale for an empty group. Label will be used for locale. + if (root_element->GetChildElements().empty()) { + ConfigDescription config_descriptor; + bool parsed = ConfigDescription::Parse(label, &config_descriptor); + if (parsed && + (config_descriptor.CopyWithoutSdkVersion().diff(ConfigDescription::DefaultConfig()) == + android::ResTable_config::CONFIG_LOCALE)) { + // Copy the locale with the minimum SDK version stripped out. + group.push_back(config_descriptor.CopyWithoutSdkVersion()); + } else { + diag->Error(DiagMessage() + << "Could not parse config descriptor for empty screen-density-group: " + << label); + valid = false; + } + + return valid; + } + for (auto* child : root_element->GetChildElements()) { if (child->name != "locale") { diag->Error(DiagMessage() << "Unexpected root_element in screen density group: " @@ -626,61 +666,58 @@ bool LocaleGroupTagHandler(PostProcessingConfiguration* config, Element* root_el return valid; }; -bool AndroidSdkGroupTagHandler(PostProcessingConfiguration* config, Element* root_element, - IDiagnostics* diag) { - std::string label = GetLabel(root_element, diag); - if (label.empty()) { - return false; - } - +bool AndroidSdkTagHandler(PostProcessingConfiguration* config, Element* root_element, + IDiagnostics* diag) { + AndroidSdk entry = AndroidSdk::ForMinSdk(-1); bool valid = true; - bool found = false; + for (const auto& attr : root_element->attributes) { + bool valid_attr = false; + if (attr.name == "label") { + entry.label = attr.value; + valid_attr = true; + } else if (attr.name == "minSdkVersion") { + Maybe<int> version = ResourceUtils::ParseSdkVersion(attr.value); + if (version) { + valid_attr = true; + entry.min_sdk_version = version.value(); + } + } else if (attr.name == "targetSdkVersion") { + Maybe<int> version = ResourceUtils::ParseSdkVersion(attr.value); + if (version) { + valid_attr = true; + entry.target_sdk_version = version; + } + } else if (attr.name == "maxSdkVersion") { + Maybe<int> version = ResourceUtils::ParseSdkVersion(attr.value); + if (version) { + valid_attr = true; + entry.max_sdk_version = version; + } + } - for (auto* child : root_element->GetChildElements()) { - if (child->name != "android-sdk") { - diag->Error(DiagMessage() << "Unexpected root_element in ABI group: " << child->name); + if (!valid_attr) { + diag->Error(DiagMessage() << "Invalid attribute: " << attr.name << " = " << attr.value); valid = false; - } else { - AndroidSdk entry; - for (const auto& attr : child->attributes) { - Maybe<int>* target = nullptr; - if (attr.name == "minSdkVersion") { - target = &entry.min_sdk_version; - } else if (attr.name == "targetSdkVersion") { - target = &entry.target_sdk_version; - } else if (attr.name == "maxSdkVersion") { - target = &entry.max_sdk_version; - } else { - diag->Warn(DiagMessage() << "Unknown attribute: " << attr.name << " = " << attr.value); - continue; - } - - *target = ResourceUtils::ParseSdkVersion(attr.value); - if (!*target) { - diag->Error(DiagMessage() << "Invalid attribute: " << attr.name << " = " << attr.value); - valid = false; - } - } + } + } - // TODO: Fill in the manifest details when they are finalised. - for (auto node : child->GetChildElements()) { - if (node->name == "manifest") { - if (entry.manifest) { - diag->Warn(DiagMessage() << "Found multiple manifest tags. Ignoring duplicates."); - continue; - } - entry.manifest = {AndroidManifest()}; - } - } + if (entry.min_sdk_version == -1) { + diag->Error(DiagMessage() << "android-sdk is missing minSdkVersion attribute"); + valid = false; + } - config->android_sdk_groups[label] = entry; - if (found) { - valid = false; + // TODO: Fill in the manifest details when they are finalised. + for (auto node : root_element->GetChildElements()) { + if (node->name == "manifest") { + if (entry.manifest) { + diag->Warn(DiagMessage() << "Found multiple manifest tags. Ignoring duplicates."); + continue; } - found = true; + entry.manifest = {AndroidManifest()}; } } + config->android_sdks[entry.label] = entry; return valid; }; @@ -691,7 +728,7 @@ bool GlTextureGroupTagHandler(PostProcessingConfiguration* config, Element* root return false; } - auto& group = config->gl_texture_groups[label]; + auto& group = GetOrCreateGroup(label, &config->gl_texture_groups); bool valid = true; GlTexture result; @@ -734,7 +771,7 @@ bool DeviceFeatureGroupTagHandler(PostProcessingConfiguration* config, Element* return false; } - auto& group = config->device_feature_groups[label]; + auto& group = GetOrCreateGroup(label, &config->device_feature_groups); bool valid = true; for (auto* child : root_element->GetChildElements()) { diff --git a/tools/aapt2/configuration/ConfigurationParser.h b/tools/aapt2/configuration/ConfigurationParser.h index ca5891025c92..7f1d4453f9b3 100644 --- a/tools/aapt2/configuration/ConfigurationParser.h +++ b/tools/aapt2/configuration/ConfigurationParser.h @@ -71,7 +71,8 @@ struct AndroidManifest { }; struct AndroidSdk { - Maybe<int> min_sdk_version; + std::string label; + int min_sdk_version; // min_sdk_version is mandatory if splitting by SDK. Maybe<int> target_sdk_version; Maybe<int> max_sdk_version; Maybe<AndroidManifest> manifest; @@ -113,15 +114,19 @@ struct OutputArtifact { Maybe<AndroidSdk> android_sdk; std::vector<DeviceFeature> features; std::vector<GlTexture> textures; + + inline int GetMinSdk(int default_value = -1) const { + if (!android_sdk) { + return default_value; + } + return android_sdk.value().min_sdk_version; + } }; } // namespace configuration // Forward declaration of classes used in the API. struct IDiagnostics; -namespace xml { -class Element; -} /** * XML configuration file parser for the split and optimize commands. @@ -133,8 +138,8 @@ class ConfigurationParser { static Maybe<ConfigurationParser> ForPath(const std::string& path); /** Returns a ConfigurationParser for the configuration in the provided file contents. */ - static ConfigurationParser ForContents(const std::string& contents) { - ConfigurationParser parser{contents}; + static ConfigurationParser ForContents(const std::string& contents, const std::string& path) { + ConfigurationParser parser{contents, path}; return parser; } @@ -156,7 +161,7 @@ class ConfigurationParser { * diagnostics context. The default diagnostics context can be overridden with a call to * WithDiagnostics(IDiagnostics *). */ - explicit ConfigurationParser(std::string contents); + ConfigurationParser(std::string contents, const std::string& config_path); /** Returns the current diagnostics context to any subclasses. */ IDiagnostics* diagnostics() { @@ -166,6 +171,8 @@ class ConfigurationParser { private: /** The contents of the configuration file to parse. */ const std::string contents_; + /** Path to the input configuration. */ + const std::string config_path_; /** The diagnostics context to send messages to. */ IDiagnostics* diag_; }; diff --git a/tools/aapt2/configuration/ConfigurationParser.internal.h b/tools/aapt2/configuration/ConfigurationParser.internal.h index 7657ebd70a8b..a583057427e6 100644 --- a/tools/aapt2/configuration/ConfigurationParser.internal.h +++ b/tools/aapt2/configuration/ConfigurationParser.internal.h @@ -17,35 +17,105 @@ #ifndef AAPT2_CONFIGURATIONPARSER_INTERNAL_H #define AAPT2_CONFIGURATIONPARSER_INTERNAL_H +#include "configuration/ConfigurationParser.h" + +#include <algorithm> +#include <limits> + namespace aapt { + +// Forward declaration of classes used in the API. +namespace xml { +class Element; +} + namespace configuration { +template <typename T> +struct OrderedEntry { + size_t order; + std::vector<T> entry; +}; + /** A mapping of group labels to group of configuration items. */ template <class T> -using Group = std::unordered_map<std::string, std::vector<T>>; +using Group = std::unordered_map<std::string, OrderedEntry<T>>; /** A mapping of group label to a single configuration item. */ template <class T> using Entry = std::unordered_map<std::string, T>; +/** Retrieves an entry from the provided Group, creating a new instance if one does not exist. */ +template <typename T> +std::vector<T>& GetOrCreateGroup(std::string label, Group<T>* group) { + OrderedEntry<T>& entry = (*group)[label]; + // If this is a new entry, set the order. + if (entry.order == 0) { + entry.order = group->size(); + } + return entry.entry; +} + +/** + * A ComparisonChain is a grouping of comparisons to perform when sorting groups that have a well + * defined order of precedence. Comparisons are only made if none of the previous comparisons had a + * definite result. A comparison has a result if at least one of the items has an entry for that + * value and that they are not equal. + */ +class ComparisonChain { + public: + /** + * Adds a new comparison of items in a group to the chain. The new comparison is only used if we + * have not been able to determine the sort order with the previous comparisons. + */ + template <typename T> + ComparisonChain& Add(const Group<T>& groups, const Maybe<std::string>& lhs, + const Maybe<std::string>& rhs) { + return Add(GetGroupOrder(groups, lhs), GetGroupOrder(groups, rhs)); + } + + /** + * Adds a new comparison to the chain. The new comparison is only used if we have not been able to + * determine the sort order with the previous comparisons. + */ + ComparisonChain& Add(int lhs, int rhs) { + if (!has_result_) { + has_result_ = (lhs != rhs); + result_ = (lhs < rhs); + } + return *this; + } + + /** Returns true if the left hand side should come before the right hand side. */ + bool Compare() { + return result_; + } + + private: + template <typename T> + inline size_t GetGroupOrder(const Group<T>& groups, const Maybe<std::string>& label) { + if (!label) { + return std::numeric_limits<size_t>::max(); + } + return groups.at(label.value()).order; + } + + bool has_result_ = false; + bool result_ = false; +}; + /** Output artifact configuration options. */ struct ConfiguredArtifact { /** Name to use for output of processing foo.apk -> foo.<name>.apk. */ Maybe<std::string> name; - /** - * Value to add to the base Android manifest versionCode. If it is not present in the - * configuration file, it is set to the previous artifact + 1. If the first artifact does not have - * a value, artifacts are a 1 based index. - */ - int version; /** If present, uses the ABI group with this name. */ Maybe<std::string> abi_group; /** If present, uses the screen density group with this name. */ Maybe<std::string> screen_density_group; /** If present, uses the locale group with this name. */ Maybe<std::string> locale_group; - /** If present, uses the Android SDK group with this name. */ - Maybe<std::string> android_sdk_group; + /** If present, uses the Android SDK with this name. */ + Maybe<std::string> android_sdk; /** If present, uses the device feature group with this name. */ Maybe<std::string> device_feature_group; /** If present, uses the OpenGL texture group with this name. */ @@ -57,31 +127,71 @@ struct ConfiguredArtifact { /** Convert an artifact name template into a name string based on configuration contents. */ Maybe<std::string> Name(const android::StringPiece& apk_name, IDiagnostics* diag) const; - - bool operator<(const ConfiguredArtifact& rhs) const { - // TODO(safarmer): Order by play store multi-APK requirements. - return version < rhs.version; - } - - bool operator==(const ConfiguredArtifact& rhs) const { - return version == rhs.version; - } }; /** AAPT2 XML configuration file binary representation. */ struct PostProcessingConfiguration { - // TODO: Support named artifacts? std::vector<ConfiguredArtifact> artifacts; Maybe<std::string> artifact_format; Group<Abi> abi_groups; Group<ConfigDescription> screen_density_groups; Group<ConfigDescription> locale_groups; - Entry<AndroidSdk> android_sdk_groups; Group<DeviceFeature> device_feature_groups; Group<GlTexture> gl_texture_groups; + Entry<AndroidSdk> android_sdks; + + /** + * Sorts the configured artifacts based on the ordering of the groups in the configuration file. + * The only exception to this rule is Android SDK versions. Larger SDK versions will have a larger + * versionCode to ensure users get the correct APK when they upgrade their OS. + */ + void SortArtifacts() { + std::sort(artifacts.begin(), artifacts.end(), *this); + } + + /** Comparator that ensures artifacts are in the preferred order for versionCode rewriting. */ + bool operator()(const ConfiguredArtifact& lhs, const ConfiguredArtifact& rhs) { + // Split dimensions are added in the order of precedence. Items higher in the list result in + // higher version codes. + return ComparisonChain() + // All splits with a minSdkVersion specified must be last to ensure the application will be + // updated if a user upgrades the version of Android on their device. + .Add(GetMinSdk(lhs), GetMinSdk(rhs)) + // ABI version is important, especially on x86 phones where they may begin to run in ARM + // emulation mode on newer Android versions. This allows us to ensure that the x86 version + // is installed on these devices rather than ARM. + .Add(abi_groups, lhs.abi_group, rhs.abi_group) + // The rest are in arbitrary order based on estimated usage. + .Add(screen_density_groups, lhs.screen_density_group, rhs.screen_density_group) + .Add(locale_groups, lhs.locale_group, rhs.locale_group) + .Add(gl_texture_groups, lhs.gl_texture_group, rhs.gl_texture_group) + .Add(device_feature_groups, lhs.device_feature_group, rhs.device_feature_group) + .Compare(); + } + + private: + /** + * Returns the min_sdk_version from the provided artifact or 0 if none is present. This allows + * artifacts that have an Android SDK version to have a higher versionCode than those that do not. + */ + inline int GetMinSdk(const ConfiguredArtifact& artifact) { + if (!artifact.android_sdk) { + return 0; + } + const auto& entry = android_sdks.find(artifact.android_sdk.value()); + if (entry == android_sdks.end()) { + return 0; + } + return entry->second.min_sdk_version; + } }; +/** Parses the provided XML document returning the post processing configuration. */ +Maybe<PostProcessingConfiguration> ExtractConfiguration(const std::string& contents, + const std::string& config_path, + IDiagnostics* diag); + namespace handler { /** Handler for <artifact> tags. */ @@ -104,9 +214,9 @@ bool ScreenDensityGroupTagHandler(configuration::PostProcessingConfiguration* co bool LocaleGroupTagHandler(configuration::PostProcessingConfiguration* config, xml::Element* element, IDiagnostics* diag); -/** Handler for <android-sdk-group> tags. */ -bool AndroidSdkGroupTagHandler(configuration::PostProcessingConfiguration* config, - xml::Element* element, IDiagnostics* diag); +/** Handler for <android-sdk> tags. */ +bool AndroidSdkTagHandler(configuration::PostProcessingConfiguration* config, xml::Element* element, + IDiagnostics* diag); /** Handler for <gl-texture-group> tags. */ bool GlTextureGroupTagHandler(configuration::PostProcessingConfiguration* config, diff --git a/tools/aapt2/configuration/ConfigurationParser_test.cpp b/tools/aapt2/configuration/ConfigurationParser_test.cpp index 3f356d78bbfe..0329846a5bb5 100644 --- a/tools/aapt2/configuration/ConfigurationParser_test.cpp +++ b/tools/aapt2/configuration/ConfigurationParser_test.cpp @@ -30,11 +30,33 @@ namespace aapt { namespace configuration { void PrintTo(const AndroidSdk& sdk, std::ostream* os) { - *os << "SDK: min=" << sdk.min_sdk_version.value_or_default(-1) + *os << "SDK: min=" << sdk.min_sdk_version << ", target=" << sdk.target_sdk_version.value_or_default(-1) << ", max=" << sdk.max_sdk_version.value_or_default(-1); } +bool operator==(const ConfiguredArtifact& lhs, const ConfiguredArtifact& rhs) { + return lhs.name == rhs.name && lhs.abi_group == rhs.abi_group && + lhs.screen_density_group == rhs.screen_density_group && + lhs.locale_group == rhs.locale_group && lhs.android_sdk == rhs.android_sdk && + lhs.device_feature_group == rhs.device_feature_group && + lhs.gl_texture_group == rhs.gl_texture_group; +} + +std::ostream& operator<<(std::ostream& out, const Maybe<std::string>& value) { + PrintTo(value, &out); + return out; +} + +void PrintTo(const ConfiguredArtifact& artifact, std::ostream* os) { + *os << "\n{" + << "\n name: " << artifact.name << "\n sdk: " << artifact.android_sdk + << "\n abi: " << artifact.abi_group << "\n density: " << artifact.screen_density_group + << "\n locale: " << artifact.locale_group + << "\n features: " << artifact.device_feature_group + << "\n textures: " << artifact.gl_texture_group << "\n}\n"; +} + namespace handler { namespace { @@ -44,6 +66,7 @@ using ::aapt::configuration::AndroidManifest; using ::aapt::configuration::AndroidSdk; using ::aapt::configuration::ConfiguredArtifact; using ::aapt::configuration::DeviceFeature; +using ::aapt::configuration::ExtractConfiguration; using ::aapt::configuration::GlTexture; using ::aapt::configuration::Locale; using ::aapt::configuration::PostProcessingConfiguration; @@ -52,11 +75,13 @@ using ::aapt::xml::NodeCast; using ::android::ResTable_config; using ::android::base::StringPrintf; using ::testing::ElementsAre; +using ::testing::Eq; using ::testing::SizeIs; +using ::testing::StrEq; constexpr const char* kValidConfig = R"(<?xml version="1.0" encoding="utf-8" ?> <post-process xmlns="http://schemas.android.com/tools/aapt"> - <groups> + <abi-groups> <abi-group label="arm"> <abi>armeabi-v7a</abi> <abi>arm64-v8a</abi> @@ -65,6 +90,8 @@ constexpr const char* kValidConfig = R"(<?xml version="1.0" encoding="utf-8" ?> <abi>x86</abi> <abi>mips</abi> </abi-group> + </abi-groups> + <screen-density-groups> <screen-density-group label="large"> <screen-density>xhdpi</screen-density> <screen-density>xxhdpi</screen-density> @@ -78,6 +105,8 @@ constexpr const char* kValidConfig = R"(<?xml version="1.0" encoding="utf-8" ?> <screen-density>xxhdpi</screen-density> <screen-density>xxxhdpi</screen-density> </screen-density-group> + </screen-density-groups> + <locale-groups> <locale-group label="europe"> <locale>en</locale> <locale>es</locale> @@ -89,25 +118,30 @@ constexpr const char* kValidConfig = R"(<?xml version="1.0" encoding="utf-8" ?> <locale>es-rMX</locale> <locale>fr-rCA</locale> </locale-group> - <android-sdk-group label="v19"> - <android-sdk - minSdkVersion="19" - targetSdkVersion="24" - maxSdkVersion="25"> - <manifest> - <!--- manifest additions here XSLT? TODO --> - </manifest> - </android-sdk> - </android-sdk-group> + </locale-groups> + <android-sdks> + <android-sdk + label="v19" + minSdkVersion="19" + targetSdkVersion="24" + maxSdkVersion="25"> + <manifest> + <!--- manifest additions here XSLT? TODO --> + </manifest> + </android-sdk> + </android-sdks> + <gl-texture-groups> <gl-texture-group label="dxt1"> <gl-texture name="GL_EXT_texture_compression_dxt1"> <texture-path>assets/dxt1/*</texture-path> </gl-texture> </gl-texture-group> + </gl-texture-groups> + <device-feature-groups> <device-feature-group label="low-latency"> <supports-feature>android.hardware.audio.low_latency</supports-feature> </device-feature-group> - </groups> + </device-feature-groups> <artifacts> <artifact-format> ${base}.${abi}.${screen-density}.${locale}.${sdk}.${gl}.${feature}.release @@ -117,7 +151,7 @@ constexpr const char* kValidConfig = R"(<?xml version="1.0" encoding="utf-8" ?> abi-group="arm" screen-density-group="large" locale-group="europe" - android-sdk-group="v19" + android-sdk="v19" gl-texture-group="dxt1" device-feature-group="low-latency"/> <artifact @@ -125,7 +159,7 @@ constexpr const char* kValidConfig = R"(<?xml version="1.0" encoding="utf-8" ?> abi-group="other" screen-density-group="alldpi" locale-group="north-america" - android-sdk-group="v19" + android-sdk="v19" gl-texture-group="dxt1" device-feature-group="low-latency"/> </artifacts> @@ -134,7 +168,8 @@ constexpr const char* kValidConfig = R"(<?xml version="1.0" encoding="utf-8" ?> class ConfigurationParserTest : public ConfigurationParser, public ::testing::Test { public: - ConfigurationParserTest() : ConfigurationParser("") {} + ConfigurationParserTest() : ConfigurationParser("", "config.xml") { + } protected: StdErrDiagnostics diag_; @@ -145,8 +180,31 @@ TEST_F(ConfigurationParserTest, ForPath_NoFile) { EXPECT_FALSE(result); } +TEST_F(ConfigurationParserTest, ExtractConfiguration) { + Maybe<PostProcessingConfiguration> maybe_config = + ExtractConfiguration(kValidConfig, "dummy.xml", &diag_); + + PostProcessingConfiguration config = maybe_config.value(); + + auto& arm = config.abi_groups["arm"]; + auto& other = config.abi_groups["other"]; + EXPECT_EQ(arm.order, 1ul); + EXPECT_EQ(other.order, 2ul); + + auto& large = config.screen_density_groups["large"]; + auto& alldpi = config.screen_density_groups["alldpi"]; + EXPECT_EQ(large.order, 1ul); + EXPECT_EQ(alldpi.order, 2ul); + + auto& north_america = config.locale_groups["north-america"]; + auto& europe = config.locale_groups["europe"]; + // Checked in reverse to make sure access order does not matter. + EXPECT_EQ(north_america.order, 2ul); + EXPECT_EQ(europe.order, 1ul); +} + TEST_F(ConfigurationParserTest, ValidateFile) { - auto parser = ConfigurationParser::ForContents(kValidConfig).WithDiagnostics(&diag_); + auto parser = ConfigurationParser::ForContents(kValidConfig, "conf.xml").WithDiagnostics(&diag_); auto result = parser.Parse("test.apk"); ASSERT_TRUE(result); const std::vector<OutputArtifact>& value = result.value(); @@ -154,6 +212,7 @@ TEST_F(ConfigurationParserTest, ValidateFile) { const OutputArtifact& a1 = value[0]; EXPECT_EQ(a1.name, "art1.apk"); + EXPECT_EQ(a1.version, 1); EXPECT_THAT(a1.abis, ElementsAre(Abi::kArmV7a, Abi::kArm64V8a)); EXPECT_THAT(a1.screen_densities, ElementsAre(test::ParseConfigOrDie("xhdpi").CopyWithoutSdkVersion(), @@ -161,12 +220,15 @@ TEST_F(ConfigurationParserTest, ValidateFile) { test::ParseConfigOrDie("xxxhdpi").CopyWithoutSdkVersion())); EXPECT_THAT(a1.locales, ElementsAre(test::ParseConfigOrDie("en"), test::ParseConfigOrDie("es"), test::ParseConfigOrDie("fr"), test::ParseConfigOrDie("de"))); - EXPECT_EQ(a1.android_sdk.value().min_sdk_version.value(), 19l); + ASSERT_TRUE(a1.android_sdk); + ASSERT_TRUE(a1.android_sdk.value().min_sdk_version); + EXPECT_EQ(a1.android_sdk.value().min_sdk_version, 19l); EXPECT_THAT(a1.textures, SizeIs(1ul)); EXPECT_THAT(a1.features, SizeIs(1ul)); const OutputArtifact& a2 = value[1]; EXPECT_EQ(a2.name, "art2.apk"); + EXPECT_EQ(a2.version, 2); EXPECT_THAT(a2.abis, ElementsAre(Abi::kX86, Abi::kMips)); EXPECT_THAT(a2.screen_densities, ElementsAre(test::ParseConfigOrDie("ldpi").CopyWithoutSdkVersion(), @@ -178,124 +240,138 @@ TEST_F(ConfigurationParserTest, ValidateFile) { EXPECT_THAT(a2.locales, ElementsAre(test::ParseConfigOrDie("en"), test::ParseConfigOrDie("es-rMX"), test::ParseConfigOrDie("fr-rCA"))); - EXPECT_EQ(a2.android_sdk.value().min_sdk_version.value(), 19l); + ASSERT_TRUE(a2.android_sdk); + ASSERT_TRUE(a2.android_sdk.value().min_sdk_version); + EXPECT_EQ(a2.android_sdk.value().min_sdk_version, 19l); EXPECT_THAT(a2.textures, SizeIs(1ul)); EXPECT_THAT(a2.features, SizeIs(1ul)); } +TEST_F(ConfigurationParserTest, ConfiguredArtifactOrdering) { + // Create a base builder with the configuration groups but no artifacts to allow it to be copied. + test::PostProcessingConfigurationBuilder base_builder = test::PostProcessingConfigurationBuilder() + .AddAbiGroup("arm") + .AddAbiGroup("arm64") + .AddAndroidSdk("v23", 23) + .AddAndroidSdk("v19", 19); + + { + // Test version ordering. + ConfiguredArtifact v23; + v23.android_sdk = {"v23"}; + ConfiguredArtifact v19; + v19.android_sdk = {"v19"}; + + test::PostProcessingConfigurationBuilder builder = base_builder; + PostProcessingConfiguration config = builder.AddArtifact(v23).AddArtifact(v19).Build(); + + config.SortArtifacts(); + ASSERT_THAT(config.artifacts, SizeIs(2)); + EXPECT_THAT(config.artifacts[0], Eq(v19)); + EXPECT_THAT(config.artifacts[1], Eq(v23)); + } + + { + // Test ABI ordering. + ConfiguredArtifact arm; + arm.android_sdk = {"v19"}; + arm.abi_group = {"arm"}; + ConfiguredArtifact arm64; + arm64.android_sdk = {"v19"}; + arm64.abi_group = {"arm64"}; + + test::PostProcessingConfigurationBuilder builder = base_builder; + PostProcessingConfiguration config = builder.AddArtifact(arm64).AddArtifact(arm).Build(); + + config.SortArtifacts(); + ASSERT_THAT(config.artifacts, SizeIs(2)); + EXPECT_THAT(config.artifacts[0], Eq(arm)); + EXPECT_THAT(config.artifacts[1], Eq(arm64)); + } + + { + // Test Android SDK has precedence over ABI. + ConfiguredArtifact arm; + arm.android_sdk = {"v23"}; + arm.abi_group = {"arm"}; + ConfiguredArtifact arm64; + arm64.android_sdk = {"v19"}; + arm64.abi_group = {"arm64"}; + + test::PostProcessingConfigurationBuilder builder = base_builder; + PostProcessingConfiguration config = builder.AddArtifact(arm64).AddArtifact(arm).Build(); + + config.SortArtifacts(); + ASSERT_THAT(config.artifacts, SizeIs(2)); + EXPECT_THAT(config.artifacts[0], Eq(arm64)); + EXPECT_THAT(config.artifacts[1], Eq(arm)); + } + + { + // Test version is better than ABI. + ConfiguredArtifact arm; + arm.abi_group = {"arm"}; + ConfiguredArtifact v19; + v19.android_sdk = {"v19"}; + + test::PostProcessingConfigurationBuilder builder = base_builder; + PostProcessingConfiguration config = builder.AddArtifact(v19).AddArtifact(arm).Build(); + + config.SortArtifacts(); + ASSERT_THAT(config.artifacts, SizeIs(2)); + EXPECT_THAT(config.artifacts[0], Eq(arm)); + EXPECT_THAT(config.artifacts[1], Eq(v19)); + } + + { + // Test version is sorted higher than no version. + ConfiguredArtifact arm; + arm.abi_group = {"arm"}; + ConfiguredArtifact v19; + v19.abi_group = {"arm"}; + v19.android_sdk = {"v19"}; + + test::PostProcessingConfigurationBuilder builder = base_builder; + PostProcessingConfiguration config = builder.AddArtifact(v19).AddArtifact(arm).Build(); + + config.SortArtifacts(); + ASSERT_THAT(config.artifacts, SizeIs(2)); + EXPECT_THAT(config.artifacts[0], Eq(arm)); + EXPECT_THAT(config.artifacts[1], Eq(v19)); + } +} + TEST_F(ConfigurationParserTest, InvalidNamespace) { constexpr const char* invalid_ns = R"(<?xml version="1.0" encoding="utf-8" ?> - <post-process xmlns="http://schemas.android.com/tools/another-unknown-tool" />)"; + <post-process xmlns="http://schemas.android.com/tools/another-unknown-tool" />)"; - auto result = ConfigurationParser::ForContents(invalid_ns).Parse("test.apk"); + auto result = ConfigurationParser::ForContents(invalid_ns, "config.xml").Parse("test.apk"); ASSERT_FALSE(result); } TEST_F(ConfigurationParserTest, ArtifactAction) { PostProcessingConfiguration config; - { - const auto doc = test::BuildXmlDom(R"xml( + const auto doc = test::BuildXmlDom(R"xml( <artifact abi-group="arm" screen-density-group="large" locale-group="europe" - android-sdk-group="v19" + android-sdk="v19" gl-texture-group="dxt1" device-feature-group="low-latency"/>)xml"); - ASSERT_TRUE(ArtifactTagHandler(&config, NodeCast<Element>(doc->root.get()), &diag_)); - - EXPECT_THAT(config.artifacts, SizeIs(1ul)); - - auto& artifact = config.artifacts.back(); - EXPECT_FALSE(artifact.name); // TODO: make this fail. - EXPECT_EQ(1, artifact.version); - EXPECT_EQ("arm", artifact.abi_group.value()); - EXPECT_EQ("large", artifact.screen_density_group.value()); - EXPECT_EQ("europe", artifact.locale_group.value()); - EXPECT_EQ("v19", artifact.android_sdk_group.value()); - EXPECT_EQ("dxt1", artifact.gl_texture_group.value()); - EXPECT_EQ("low-latency", artifact.device_feature_group.value()); - } - - { - // Perform a second action to ensure we get 2 artifacts. - const auto doc = test::BuildXmlDom(R"xml( - <artifact - abi-group="other" - screen-density-group="large" - locale-group="europe" - android-sdk-group="v19" - gl-texture-group="dxt1" - device-feature-group="low-latency"/>)xml"); + ASSERT_TRUE(ArtifactTagHandler(&config, NodeCast<Element>(doc->root.get()), &diag_)); - ASSERT_TRUE(ArtifactTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_)); - EXPECT_THAT(config.artifacts, SizeIs(2ul)); - EXPECT_EQ(2, config.artifacts.back().version); - } + EXPECT_THAT(config.artifacts, SizeIs(1ul)); - { - // Perform a third action with a set version code. - const auto doc = test::BuildXmlDom(R"xml( - <artifact - version="5" - abi-group="other" - screen-density-group="large" - locale-group="europe" - android-sdk-group="v19" - gl-texture-group="dxt1" - device-feature-group="low-latency"/>)xml"); - - ASSERT_TRUE(ArtifactTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_)); - EXPECT_THAT(config.artifacts, SizeIs(3ul)); - EXPECT_EQ(5, config.artifacts.back().version); - } - - { - // Perform a fourth action to ensure the version code still increments. - const auto doc = test::BuildXmlDom(R"xml( - <artifact - abi-group="other" - screen-density-group="large" - locale-group="europe" - android-sdk-group="v19" - gl-texture-group="dxt1" - device-feature-group="low-latency"/>)xml"); - - ASSERT_TRUE(ArtifactTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_)); - EXPECT_THAT(config.artifacts, SizeIs(4ul)); - EXPECT_EQ(6, config.artifacts.back().version); - } -} - -TEST_F(ConfigurationParserTest, DuplicateArtifactVersion) { - static constexpr const char* configuration = R"xml(<?xml version="1.0" encoding="utf-8" ?> - <pst-process xmlns="http://schemas.android.com/tools/aapt">> - <artifacts> - <artifact-format> - ${base}.${abi}.${screen-density}.${locale}.${sdk}.${gl}.${feature}.release - </artifact-format> - <artifact - name="art1" - abi-group="arm" - screen-density-group="large" - locale-group="europe" - android-sdk-group="v19" - gl-texture-group="dxt1" - device-feature-group="low-latency"/> - <artifact - name="art2" - version = "1" - abi-group="other" - screen-density-group="alldpi" - locale-group="north-america" - android-sdk-group="v19" - gl-texture-group="dxt1" - device-feature-group="low-latency"/> - </artifacts> - </post-process>)xml"; - auto result = ConfigurationParser::ForContents(configuration).Parse("test.apk"); - ASSERT_FALSE(result); + auto& artifact = config.artifacts.back(); + EXPECT_FALSE(artifact.name); // TODO: make this fail. + EXPECT_EQ("arm", artifact.abi_group.value()); + EXPECT_EQ("large", artifact.screen_density_group.value()); + EXPECT_EQ("europe", artifact.locale_group.value()); + EXPECT_EQ("v19", artifact.android_sdk.value()); + EXPECT_EQ("dxt1", artifact.gl_texture_group.value()); + EXPECT_EQ("low-latency", artifact.device_feature_group.value()); } TEST_F(ConfigurationParserTest, ArtifactFormatAction) { @@ -334,10 +410,36 @@ TEST_F(ConfigurationParserTest, AbiGroupAction) { EXPECT_THAT(config.abi_groups, SizeIs(1ul)); ASSERT_EQ(1u, config.abi_groups.count("arm")); - auto& out = config.abi_groups["arm"]; + auto& out = config.abi_groups["arm"].entry; ASSERT_THAT(out, ElementsAre(Abi::kArmV7a, Abi::kArm64V8a)); } +TEST_F(ConfigurationParserTest, AbiGroupAction_EmptyGroup) { + static constexpr const char* xml = R"xml(<abi-group label="arm64-v8a"/>)xml"; + + auto doc = test::BuildXmlDom(xml); + + PostProcessingConfiguration config; + bool ok = AbiGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + EXPECT_THAT(config.abi_groups, SizeIs(1ul)); + ASSERT_EQ(1u, config.abi_groups.count("arm64-v8a")); + + auto& out = config.abi_groups["arm64-v8a"].entry; + ASSERT_THAT(out, ElementsAre(Abi::kArm64V8a)); +} + +TEST_F(ConfigurationParserTest, AbiGroupAction_InvalidEmptyGroup) { + static constexpr const char* xml = R"xml(<abi-group label="arm"/>)xml"; + + auto doc = test::BuildXmlDom(xml); + + PostProcessingConfiguration config; + bool ok = AbiGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + ASSERT_FALSE(ok); +} + TEST_F(ConfigurationParserTest, ScreenDensityGroupAction) { static constexpr const char* xml = R"xml( <screen-density-group label="large"> @@ -364,10 +466,39 @@ TEST_F(ConfigurationParserTest, ScreenDensityGroupAction) { ConfigDescription xxxhdpi; xxxhdpi.density = ResTable_config::DENSITY_XXXHIGH; - auto& out = config.screen_density_groups["large"]; + auto& out = config.screen_density_groups["large"].entry; ASSERT_THAT(out, ElementsAre(xhdpi, xxhdpi, xxxhdpi)); } +TEST_F(ConfigurationParserTest, ScreenDensityGroupAction_EmtpyGroup) { + static constexpr const char* xml = R"xml(<screen-density-group label="xhdpi"/>)xml"; + + auto doc = test::BuildXmlDom(xml); + + PostProcessingConfiguration config; + bool ok = ScreenDensityGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + EXPECT_THAT(config.screen_density_groups, SizeIs(1ul)); + ASSERT_EQ(1u, config.screen_density_groups.count("xhdpi")); + + ConfigDescription xhdpi; + xhdpi.density = ResTable_config::DENSITY_XHIGH; + + auto& out = config.screen_density_groups["xhdpi"].entry; + ASSERT_THAT(out, ElementsAre(xhdpi)); +} + +TEST_F(ConfigurationParserTest, ScreenDensityGroupAction_InvalidEmtpyGroup) { + static constexpr const char* xml = R"xml(<screen-density-group label="really-big-screen"/>)xml"; + + auto doc = test::BuildXmlDom(xml); + + PostProcessingConfiguration config; + bool ok = ScreenDensityGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + ASSERT_FALSE(ok); +} + TEST_F(ConfigurationParserTest, LocaleGroupAction) { static constexpr const char* xml = R"xml( <locale-group label="europe"> @@ -386,7 +517,7 @@ TEST_F(ConfigurationParserTest, LocaleGroupAction) { ASSERT_EQ(1ul, config.locale_groups.size()); ASSERT_EQ(1u, config.locale_groups.count("europe")); - const auto& out = config.locale_groups["europe"]; + const auto& out = config.locale_groups["europe"].entry; ConfigDescription en = test::ParseConfigOrDie("en"); ConfigDescription es = test::ParseConfigOrDie("es"); @@ -396,29 +527,56 @@ TEST_F(ConfigurationParserTest, LocaleGroupAction) { ASSERT_THAT(out, ElementsAre(en, es, fr, de)); } +TEST_F(ConfigurationParserTest, LocaleGroupAction_EmtpyGroup) { + static constexpr const char* xml = R"xml(<locale-group label="en"/>)xml"; + + auto doc = test::BuildXmlDom(xml); + + PostProcessingConfiguration config; + bool ok = LocaleGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + ASSERT_TRUE(ok); + + ASSERT_EQ(1ul, config.locale_groups.size()); + ASSERT_EQ(1u, config.locale_groups.count("en")); + + const auto& out = config.locale_groups["en"].entry; + + ConfigDescription en = test::ParseConfigOrDie("en"); + + ASSERT_THAT(out, ElementsAre(en)); +} + +TEST_F(ConfigurationParserTest, LocaleGroupAction_InvalidEmtpyGroup) { + static constexpr const char* xml = R"xml(<locale-group label="arm64"/>)xml"; + + auto doc = test::BuildXmlDom(xml); + + PostProcessingConfiguration config; + bool ok = LocaleGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + ASSERT_FALSE(ok); +} + TEST_F(ConfigurationParserTest, AndroidSdkGroupAction) { static constexpr const char* xml = R"xml( - <android-sdk-group label="v19"> - <android-sdk + <android-sdk label="v19" minSdkVersion="19" targetSdkVersion="24" maxSdkVersion="25"> <manifest> <!--- manifest additions here XSLT? TODO --> </manifest> - </android-sdk> - </android-sdk-group>)xml"; + </android-sdk>)xml"; auto doc = test::BuildXmlDom(xml); PostProcessingConfiguration config; - bool ok = AndroidSdkGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + bool ok = AndroidSdkTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); ASSERT_TRUE(ok); - ASSERT_EQ(1ul, config.android_sdk_groups.size()); - ASSERT_EQ(1u, config.android_sdk_groups.count("v19")); + ASSERT_EQ(1ul, config.android_sdks.size()); + ASSERT_EQ(1u, config.android_sdks.count("v19")); - auto& out = config.android_sdk_groups["v19"]; + auto& out = config.android_sdks["v19"]; AndroidSdk sdk; sdk.min_sdk_version = 19; @@ -431,98 +589,86 @@ TEST_F(ConfigurationParserTest, AndroidSdkGroupAction) { TEST_F(ConfigurationParserTest, AndroidSdkGroupAction_SingleVersion) { { - static constexpr const char* xml = R"xml( - <android-sdk-group label="v19"> - <android-sdk minSdkVersion="19"></android-sdk> - </android-sdk-group>)xml"; - + const char* xml = "<android-sdk label='v19' minSdkVersion='19'></android-sdk>"; auto doc = test::BuildXmlDom(xml); PostProcessingConfiguration config; - bool ok = AndroidSdkGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + bool ok = AndroidSdkTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); ASSERT_TRUE(ok); - ASSERT_EQ(1ul, config.android_sdk_groups.size()); - ASSERT_EQ(1u, config.android_sdk_groups.count("v19")); + ASSERT_EQ(1ul, config.android_sdks.size()); + ASSERT_EQ(1u, config.android_sdks.count("v19")); - auto& out = config.android_sdk_groups["v19"]; - EXPECT_EQ(19, out.min_sdk_version.value()); + auto& out = config.android_sdks["v19"]; + EXPECT_EQ(19, out.min_sdk_version); EXPECT_FALSE(out.max_sdk_version); EXPECT_FALSE(out.target_sdk_version); } { - static constexpr const char* xml = R"xml( - <android-sdk-group label="v19"> - <android-sdk maxSdkVersion="19"></android-sdk> - </android-sdk-group>)xml"; - + const char* xml = + "<android-sdk label='v19' minSdkVersion='19' maxSdkVersion='19'></android-sdk>"; auto doc = test::BuildXmlDom(xml); PostProcessingConfiguration config; - bool ok = AndroidSdkGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + bool ok = AndroidSdkTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); ASSERT_TRUE(ok); - ASSERT_EQ(1ul, config.android_sdk_groups.size()); - ASSERT_EQ(1u, config.android_sdk_groups.count("v19")); + ASSERT_EQ(1ul, config.android_sdks.size()); + ASSERT_EQ(1u, config.android_sdks.count("v19")); - auto& out = config.android_sdk_groups["v19"]; + auto& out = config.android_sdks["v19"]; EXPECT_EQ(19, out.max_sdk_version.value()); - EXPECT_FALSE(out.min_sdk_version); + EXPECT_EQ(19, out.min_sdk_version); EXPECT_FALSE(out.target_sdk_version); } { - static constexpr const char* xml = R"xml( - <android-sdk-group label="v19"> - <android-sdk targetSdkVersion="19"></android-sdk> - </android-sdk-group>)xml"; - + const char* xml = + "<android-sdk label='v19' minSdkVersion='19' targetSdkVersion='19'></android-sdk>"; auto doc = test::BuildXmlDom(xml); PostProcessingConfiguration config; - bool ok = AndroidSdkGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + bool ok = AndroidSdkTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); ASSERT_TRUE(ok); - ASSERT_EQ(1ul, config.android_sdk_groups.size()); - ASSERT_EQ(1u, config.android_sdk_groups.count("v19")); + ASSERT_EQ(1ul, config.android_sdks.size()); + ASSERT_EQ(1u, config.android_sdks.count("v19")); - auto& out = config.android_sdk_groups["v19"]; + auto& out = config.android_sdks["v19"]; EXPECT_EQ(19, out.target_sdk_version.value()); - EXPECT_FALSE(out.min_sdk_version); + EXPECT_EQ(19, out.min_sdk_version); EXPECT_FALSE(out.max_sdk_version); } } TEST_F(ConfigurationParserTest, AndroidSdkGroupAction_InvalidVersion) { static constexpr const char* xml = R"xml( - <android-sdk-group label="v19"> - <android-sdk - minSdkVersion="v19" - targetSdkVersion="v24" - maxSdkVersion="v25"> - <manifest> - <!--- manifest additions here XSLT? TODO --> - </manifest> - </android-sdk> - </android-sdk-group>)xml"; + <android-sdk + label="v19" + minSdkVersion="v19" + targetSdkVersion="v24" + maxSdkVersion="v25"> + <manifest> + <!--- manifest additions here XSLT? TODO --> + </manifest> + </android-sdk>)xml"; auto doc = test::BuildXmlDom(xml); PostProcessingConfiguration config; - bool ok = AndroidSdkGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + bool ok = AndroidSdkTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); ASSERT_FALSE(ok); } TEST_F(ConfigurationParserTest, AndroidSdkGroupAction_NonNumeric) { static constexpr const char* xml = R"xml( - <android-sdk-group label="P"> <android-sdk + label="P" minSdkVersion="25" targetSdkVersion="%s" maxSdkVersion="%s"> - </android-sdk> - </android-sdk-group>)xml"; + </android-sdk>)xml"; const auto& dev_sdk = GetDevelopmentSdkCodeNameAndVersion(); const char* codename = dev_sdk.first.data(); @@ -531,13 +677,13 @@ TEST_F(ConfigurationParserTest, AndroidSdkGroupAction_NonNumeric) { auto doc = test::BuildXmlDom(StringPrintf(xml, codename, codename)); PostProcessingConfiguration config; - bool ok = AndroidSdkGroupTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); + bool ok = AndroidSdkTagHandler(&config, NodeCast<Element>(doc.get()->root.get()), &diag_); ASSERT_TRUE(ok); - ASSERT_EQ(1ul, config.android_sdk_groups.size()); - ASSERT_EQ(1u, config.android_sdk_groups.count("P")); + ASSERT_EQ(1ul, config.android_sdks.size()); + ASSERT_EQ(1u, config.android_sdks.count("P")); - auto& out = config.android_sdk_groups["P"]; + auto& out = config.android_sdks["P"]; AndroidSdk sdk; sdk.min_sdk_version = 25; @@ -567,7 +713,7 @@ TEST_F(ConfigurationParserTest, GlTextureGroupAction) { EXPECT_THAT(config.gl_texture_groups, SizeIs(1ul)); ASSERT_EQ(1u, config.gl_texture_groups.count("dxt1")); - auto& out = config.gl_texture_groups["dxt1"]; + auto& out = config.gl_texture_groups["dxt1"].entry; GlTexture texture{ std::string("GL_EXT_texture_compression_dxt1"), @@ -596,7 +742,7 @@ TEST_F(ConfigurationParserTest, DeviceFeatureGroupAction) { EXPECT_THAT(config.device_feature_groups, SizeIs(1ul)); ASSERT_EQ(1u, config.device_feature_groups.count("low-latency")); - auto& out = config.device_feature_groups["low-latency"]; + auto& out = config.device_feature_groups["low-latency"].entry; DeviceFeature low_latency = "android.hardware.audio.low_latency"; DeviceFeature pro = "android.hardware.audio.pro"; @@ -650,7 +796,7 @@ TEST(ArtifactTest, Complex) { artifact.device_feature_group = {"df1"}; artifact.gl_texture_group = {"glx1"}; artifact.locale_group = {"en-AU"}; - artifact.android_sdk_group = {"v26"}; + artifact.android_sdk = {"v26"}; { auto result = artifact.ToArtifactName( diff --git a/tools/aapt2/configuration/aapt2.xsd b/tools/aapt2/configuration/aapt2.xsd index 134153a017f8..fb2f49bae7b7 100644 --- a/tools/aapt2/configuration/aapt2.xsd +++ b/tools/aapt2/configuration/aapt2.xsd @@ -8,22 +8,52 @@ <xsd:element name="post-process"> <xsd:complexType> <xsd:sequence> - <xsd:element name="groups" type="groups"/> <xsd:element name="artifacts" type="artifacts"/> + <xsd:element name="android-sdks" type="android-sdks"/> + <xsd:element name="abi-groups" type="abi-groups"/> + <xsd:element name="screen-density-groups" type="screen-density-groups"/> + <xsd:element name="locale-groups" type="locale-groups"/> + <xsd:element name="gl-texture-groups" type="gl-texture-groups"/> + <xsd:element name="device-feature-groups" type="device-feature-groups"/> </xsd:sequence> </xsd:complexType> </xsd:element> - <xsd:complexType name="groups"> + <xsd:complexType name="android-sdks"> + <xsd:sequence> + <xsd:element name="android-sdk" type="android-sdk" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + + <xsd:complexType name="abi-groups"> <xsd:sequence> <xsd:element name="abi-group" type="abi-group" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + + <xsd:complexType name="screen-density-groups"> + <xsd:sequence> <xsd:element name="screen-density-group" type="screen-density-group" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + + <xsd:complexType name="locale-groups"> + <xsd:sequence> <xsd:element name="locale-group" type="locale-group" maxOccurs="unbounded"/> - <xsd:element name="android-sdk-group" type="android-sdk-group" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + + <xsd:complexType name="gl-texture-groups"> + <xsd:sequence> <xsd:element name="gl-texture-group" type="gl-texture-group" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + + <xsd:complexType name="device-feature-groups"> + <xsd:sequence> <xsd:element name="device-feature-group" type="device-feature-group" maxOccurs="unbounded"/> </xsd:sequence> </xsd:complexType> @@ -38,8 +68,6 @@ <!-- Groups output artifacts together by dimension labels. --> <xsd:complexType name="artifact"> - <xsd:attribute name="name" type="xsd:string"/> - <xsd:attribute name="version" type="xsd:integer"/> <xsd:attribute name="abi-group" type="xsd:string"/> <xsd:attribute name="android-sdk-group" type="xsd:string"/> <xsd:attribute name="device-feature-group" type="xsd:string"/> @@ -52,7 +80,7 @@ <xsd:sequence> <xsd:element name="gl-texture" type="gl-texture" maxOccurs="unbounded"/> </xsd:sequence> - <xsd:attribute name="label" type="xsd:string" use="optional"/> + <xsd:attribute name="label" type="xsd:string"/> </xsd:complexType> <xsd:complexType name="gl-texture"> @@ -66,14 +94,14 @@ <xsd:sequence> <xsd:element name="supports-feature" type="xsd:string" maxOccurs="unbounded"/> </xsd:sequence> - <xsd:attribute name="label" type="xsd:string" use="optional"/> + <xsd:attribute name="label" type="xsd:string"/> </xsd:complexType> <xsd:complexType name="abi-group"> <xsd:sequence> <xsd:element name="abi" type="abi-name" maxOccurs="unbounded"/> </xsd:sequence> - <xsd:attribute name="label" type="xsd:string" use="optional"/> + <xsd:attribute name="label" type="xsd:string"/> </xsd:complexType> <xsd:simpleType name="abi-name"> @@ -93,7 +121,7 @@ <xsd:sequence> <xsd:element name="screen-density" type="screen-density" maxOccurs="unbounded"/> </xsd:sequence> - <xsd:attribute name="label" type="xsd:string" use="optional"/> + <xsd:attribute name="label" type="xsd:string"/> </xsd:complexType> <xsd:simpleType name="screen-density"> @@ -108,20 +136,14 @@ </xsd:restriction> </xsd:simpleType> - <xsd:complexType name="android-sdk-group"> - <xsd:sequence> - <xsd:element name="android-sdk" type="android-sdk" maxOccurs="unbounded"/> - </xsd:sequence> - <xsd:attribute name="label" type="xsd:string" use="optional"/> - </xsd:complexType> - <xsd:complexType name="android-sdk"> <!-- TODO(safarmer): Add permissions to add/remove. --> <!-- TODO(safarmer): Add option for uncompressed native libs. --> <xsd:sequence> <xsd:element name="manifest" type="manifest"/> </xsd:sequence> - <xsd:attribute name="minSdkVersion" type="xsd:integer"/> + <xsd:attribute name="label" type="xsd:string" use="required"/> + <xsd:attribute name="minSdkVersion" type="xsd:integer" use="required"/> <xsd:attribute name="targetSdkVersion" type="xsd:integer"/> <xsd:attribute name="maxSdkVersion" type="xsd:integer"/> </xsd:complexType> @@ -135,7 +157,7 @@ <xsd:sequence> <xsd:element name="locale" type="locale" maxOccurs="unbounded"/> </xsd:sequence> - <xsd:attribute name="label" type="xsd:string" use="optional"/> + <xsd:attribute name="label" type="xsd:string"/> </xsd:complexType> <xsd:complexType name="locale"> diff --git a/tools/aapt2/configuration/example/config.xml b/tools/aapt2/configuration/example/config.xml index ce31e61b2d62..d8aba09e836d 100644 --- a/tools/aapt2/configuration/example/config.xml +++ b/tools/aapt2/configuration/example/config.xml @@ -1,6 +1,41 @@ <?xml version="1.0" encoding="utf-8" ?> <post-process xmlns="http://schemas.android.com/tools/aapt"> - <groups> + <artifacts> + <artifact-format> + ${base}.${abi}.${screen-density}.${locale}.${sdk}.${gl}.${feature}.release + </artifact-format> + + <artifact + abi-group="arm" + screen-density-group="large" + locale-group="europe" + android-sdk-group="19" + gl-texture-group="dxt1" + device-feature-group="low-latency"/> + + <artifact + abi-group="other" + screen-density-group="alldpi" + locale-group="north-america" + android-sdk-group="19" + gl-texture-group="dxt1" + device-feature-group="low-latency"/> + + </artifacts> + + <android-sdks> + <android-sdk + label="19" + minSdkVersion="19" + targetSdkVersion="24" + maxSdkVersion="25"> + <manifest> + <!--- manifest additions here XSLT? TODO --> + </manifest> + </android-sdk> + </android-sdks> + + <abi-groups> <abi-group label="arm"> <abi>armeabi-v7a</abi> <abi>arm64-v8a</abi> @@ -10,7 +45,9 @@ <abi>x86</abi> <abi>mips</abi> </abi-group> + </abi-groups> + <screen-density-groups> <screen-density-group label="large"> <screen-density>xhdpi</screen-density> <screen-density>xxhdpi</screen-density> @@ -25,7 +62,9 @@ <screen-density>xxhdpi</screen-density> <screen-density>xxxhdpi</screen-density> </screen-density-group> + </screen-density-groups> + <locale-groups> <locale-group label="europe"> <locale lang="en"/> <locale lang="es"/> @@ -42,49 +81,20 @@ <locale-group label="all"> <locale compressed="true"/> </locale-group> + </locale-groups> - <android-sdk-group label="19"> - <android-sdk - minSdkVersion="19" - targetSdkVersion="24" - maxSdkVersion="25"> - <manifest> - <!--- manifest additions here XSLT? TODO --> - </manifest> - </android-sdk> - </android-sdk-group> - + <gl-texture-groups> <gl-texture-group label="dxt1"> <gl-texture name="GL_EXT_texture_compression_dxt1"> <texture-path>assets/dxt1/*</texture-path> </gl-texture> </gl-texture-group> + </gl-texture-groups> + <device-feature-groups> <device-feature-group label="low-latency"> <supports-feature>android.hardware.audio.low_latency</supports-feature> </device-feature-group> - </groups> - - <artifacts> - <artifact-format> - ${base}.${abi}.${screen-density}.${locale}.${sdk}.${gl}.${feature}.release - </artifact-format> + </device-feature-groups> - <artifact - abi-group="arm" - screen-density-group="large" - locale-group="europe" - android-sdk-group="19" - gl-texture-group="dxt1" - device-feature-group="low-latency"/> - - <artifact - abi-group="other" - screen-density-group="alldpi" - locale-group="north-america" - android-sdk-group="19" - gl-texture-group="dxt1" - device-feature-group="low-latency"/> - - </artifacts> </post-process> diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp index 5078678e08a3..8d079ffecb63 100644 --- a/tools/aapt2/format/binary/BinaryResourceParser.cpp +++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp @@ -223,7 +223,7 @@ bool BinaryResourceParser::ParsePackage(const ResChunk_header* chunk) { break; case android::RES_TABLE_TYPE_SPEC_TYPE: - if (!ParseTypeSpec(parser.chunk())) { + if (!ParseTypeSpec(package, parser.chunk())) { return false; } break; @@ -260,7 +260,8 @@ bool BinaryResourceParser::ParsePackage(const ResChunk_header* chunk) { return true; } -bool BinaryResourceParser::ParseTypeSpec(const ResChunk_header* chunk) { +bool BinaryResourceParser::ParseTypeSpec(const ResourceTablePackage* package, + const ResChunk_header* chunk) { if (type_pool_.getError() != NO_ERROR) { diag_->Error(DiagMessage(source_) << "missing type string pool"); return false; @@ -276,6 +277,34 @@ bool BinaryResourceParser::ParseTypeSpec(const ResChunk_header* chunk) { diag_->Error(DiagMessage(source_) << "ResTable_typeSpec has invalid id: " << type_spec->id); return false; } + + // The data portion of this chunk contains entry_count 32bit entries, + // each one representing a set of flags. + const size_t entry_count = dtohl(type_spec->entryCount); + + // There can only be 2^16 entries in a type, because that is the ID + // space for entries (EEEE) in the resource ID 0xPPTTEEEE. + if (entry_count > std::numeric_limits<uint16_t>::max()) { + diag_->Error(DiagMessage(source_) + << "ResTable_typeSpec has too many entries (" << entry_count << ")"); + return false; + } + + const size_t data_size = util::DeviceToHost32(type_spec->header.size) - + util::DeviceToHost16(type_spec->header.headerSize); + if (entry_count * sizeof(uint32_t) > data_size) { + diag_->Error(DiagMessage(source_) << "ResTable_typeSpec too small to hold entries."); + return false; + } + + // Record the type_spec_flags for later. We don't know resource names yet, and we need those + // to mark resources as overlayable. + const uint32_t* type_spec_flags = reinterpret_cast<const uint32_t*>( + reinterpret_cast<uintptr_t>(type_spec) + util::DeviceToHost16(type_spec->header.headerSize)); + for (size_t i = 0; i < entry_count; i++) { + ResourceId id(package->id.value_or_default(0x0), type_spec->id, static_cast<size_t>(i)); + entry_type_spec_flags_[id] = util::DeviceToHost32(type_spec_flags[i]); + } return true; } @@ -346,18 +375,34 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package, return false; } - if (!table_->AddResourceAllowMangled(name, res_id, config, {}, std::move(resource_value), - diag_)) { + if (!table_->AddResourceWithIdMangled(name, res_id, config, {}, std::move(resource_value), + diag_)) { return false; } - if ((entry->flags & ResTable_entry::FLAG_PUBLIC) != 0) { - Symbol symbol; - symbol.state = SymbolState::kPublic; - symbol.source = source_.WithLine(0); - if (!table_->SetSymbolStateAllowMangled(name, res_id, symbol, diag_)) { - return false; + const uint32_t type_spec_flags = entry_type_spec_flags_[res_id]; + if ((entry->flags & ResTable_entry::FLAG_PUBLIC) != 0 || + (type_spec_flags & ResTable_typeSpec::SPEC_OVERLAYABLE) != 0) { + if (entry->flags & ResTable_entry::FLAG_PUBLIC) { + Visibility visibility; + visibility.level = Visibility::Level::kPublic; + visibility.source = source_.WithLine(0); + if (!table_->SetVisibilityWithIdMangled(name, visibility, res_id, diag_)) { + return false; + } + } + + if (type_spec_flags & ResTable_typeSpec::SPEC_OVERLAYABLE) { + Overlayable overlayable; + overlayable.source = source_.WithLine(0); + if (!table_->SetOverlayableMangled(name, overlayable, diag_)) { + return false; + } } + + // Erase the ID from the map once processed, so that we don't mark the same symbol more than + // once. + entry_type_spec_flags_.erase(res_id); } // Add this resource name->id mapping to the index so diff --git a/tools/aapt2/format/binary/BinaryResourceParser.h b/tools/aapt2/format/binary/BinaryResourceParser.h index 052f806e3b95..a1f9f83edfb6 100644 --- a/tools/aapt2/format/binary/BinaryResourceParser.h +++ b/tools/aapt2/format/binary/BinaryResourceParser.h @@ -50,7 +50,7 @@ class BinaryResourceParser { bool ParseTable(const android::ResChunk_header* chunk); bool ParsePackage(const android::ResChunk_header* chunk); - bool ParseTypeSpec(const android::ResChunk_header* chunk); + bool ParseTypeSpec(const ResourceTablePackage* package, const android::ResChunk_header* chunk); bool ParseType(const ResourceTablePackage* package, const android::ResChunk_header* chunk); bool ParseLibrary(const android::ResChunk_header* chunk); @@ -105,6 +105,9 @@ class BinaryResourceParser { // A mapping of resource ID to resource name. When we finish parsing // we use this to convert all resource IDs to symbolic references. std::map<ResourceId, ResourceName> id_index_; + + // A mapping of resource ID to type spec flags. + std::unordered_map<ResourceId, uint32_t> entry_type_spec_flags_; }; } // namespace aapt diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index a3034df91d82..24a4112e181d 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -283,7 +283,7 @@ class PackageFlattener { T* result = buffer->NextBlock<T>(); ResTable_entry* out_entry = (ResTable_entry*)result; - if (entry->entry->symbol_status.state == SymbolState::kPublic) { + if (entry->entry->visibility.level == Visibility::Level::kPublic) { out_entry->flags |= ResTable_entry::FLAG_PUBLIC; } @@ -443,10 +443,15 @@ class PackageFlattener { // Populate the config masks for this entry. - if (entry->symbol_status.state == SymbolState::kPublic) { + if (entry->visibility.level == Visibility::Level::kPublic) { config_masks[entry->id.value()] |= util::HostToDevice32(ResTable_typeSpec::SPEC_PUBLIC); } + if (entry->overlayable) { + config_masks[entry->id.value()] |= + util::HostToDevice32(ResTable_typeSpec::SPEC_OVERLAYABLE); + } + const size_t config_count = entry->values.size(); for (size_t i = 0; i < config_count; i++) { const ConfigDescription& config = entry->values[i]->config; diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp index f0b80d22a9b4..51ccdc7359b2 100644 --- a/tools/aapt2/format/binary/TableFlattener_test.cpp +++ b/tools/aapt2/format/binary/TableFlattener_test.cpp @@ -26,6 +26,7 @@ using namespace android; +using ::testing::Gt; using ::testing::IsNull; using ::testing::NotNull; @@ -250,15 +251,15 @@ static std::unique_ptr<ResourceTable> BuildTableWithSparseEntries( const ResourceId resid(context->GetPackageId(), 0x02, static_cast<uint16_t>(i)); const auto value = util::make_unique<BinaryPrimitive>(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(i)); - CHECK(table->AddResource(name, resid, ConfigDescription::DefaultConfig(), "", - std::unique_ptr<Value>(value->Clone(nullptr)), - context->GetDiagnostics())); + CHECK(table->AddResourceWithId(name, resid, ConfigDescription::DefaultConfig(), "", + std::unique_ptr<Value>(value->Clone(nullptr)), + context->GetDiagnostics())); // Every few entries, write out a sparse_config value. This will give us the desired load. if (i % stride == 0) { - CHECK(table->AddResource(name, resid, sparse_config, "", - std::unique_ptr<Value>(value->Clone(nullptr)), - context->GetDiagnostics())); + CHECK(table->AddResourceWithId(name, resid, sparse_config, "", + std::unique_ptr<Value>(value->Clone(nullptr)), + context->GetDiagnostics())); } } return table; @@ -568,4 +569,25 @@ TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) { ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u)); } +TEST_F(TableFlattenerTest, FlattenOverlayable) { + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .SetPackageId("com.app.test", 0x7f) + .AddSimple("com.app.test:integer/overlayable", ResourceId(0x7f020000)) + .Build(); + + ASSERT_TRUE(table->SetOverlayable(test::ParseNameOrDie("com.app.test:integer/overlayable"), + Overlayable{}, test::GetDiagnostics())); + + ResTable res_table; + ASSERT_TRUE(Flatten(context_.get(), {}, table.get(), &res_table)); + + const StringPiece16 overlayable_name(u"com.app.test:integer/overlayable"); + uint32_t spec_flags = 0u; + ASSERT_THAT(res_table.identifierForName(overlayable_name.data(), overlayable_name.size(), nullptr, + 0u, nullptr, 0u, &spec_flags), + Gt(0u)); + EXPECT_TRUE(spec_flags & android::ResTable_typeSpec::SPEC_OVERLAYABLE); +} + } // namespace aapt diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp index 0f0bce8bf5a7..3d6975d8d559 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -358,16 +358,16 @@ static void DeserializeSourceFromPb(const pb::Source& pb_source, const ResString out_source->line = static_cast<size_t>(pb_source.position().line_number()); } -static SymbolState DeserializeVisibilityFromPb(const pb::SymbolStatus_Visibility& pb_visibility) { - switch (pb_visibility) { - case pb::SymbolStatus_Visibility_PRIVATE: - return SymbolState::kPrivate; - case pb::SymbolStatus_Visibility_PUBLIC: - return SymbolState::kPublic; +static Visibility::Level DeserializeVisibilityFromPb(const pb::Visibility::Level& pb_level) { + switch (pb_level) { + case pb::Visibility::PRIVATE: + return Visibility::Level::kPrivate; + case pb::Visibility::PUBLIC: + return Visibility::Level::kPublic; default: break; } - return SymbolState::kUndefined; + return Visibility::Level::kUndefined; } static bool DeserializePackageFromPb(const pb::Package& pb_package, const ResStringPool& src_pool, @@ -402,28 +402,48 @@ static bool DeserializePackageFromPb(const pb::Package& pb_package, const ResStr } // Deserialize the symbol status (public/private with source and comments). - if (pb_entry.has_symbol_status()) { - const pb::SymbolStatus& pb_status = pb_entry.symbol_status(); - if (pb_status.has_source()) { - DeserializeSourceFromPb(pb_status.source(), src_pool, &entry->symbol_status.source); + if (pb_entry.has_visibility()) { + const pb::Visibility& pb_visibility = pb_entry.visibility(); + if (pb_visibility.has_source()) { + DeserializeSourceFromPb(pb_visibility.source(), src_pool, &entry->visibility.source); } + entry->visibility.comment = pb_visibility.comment(); - entry->symbol_status.comment = pb_status.comment(); - entry->symbol_status.allow_new = pb_status.allow_new(); - - const SymbolState visibility = DeserializeVisibilityFromPb(pb_status.visibility()); - entry->symbol_status.state = visibility; - if (visibility == SymbolState::kPublic) { + const Visibility::Level level = DeserializeVisibilityFromPb(pb_visibility.level()); + entry->visibility.level = level; + if (level == Visibility::Level::kPublic) { // Propagate the public visibility up to the Type. - type->symbol_status.state = SymbolState::kPublic; - } else if (visibility == SymbolState::kPrivate) { + type->visibility_level = Visibility::Level::kPublic; + } else if (level == Visibility::Level::kPrivate) { // Only propagate if no previous state was assigned. - if (type->symbol_status.state == SymbolState::kUndefined) { - type->symbol_status.state = SymbolState::kPrivate; + if (type->visibility_level == Visibility::Level::kUndefined) { + type->visibility_level = Visibility::Level::kPrivate; } } } + if (pb_entry.has_allow_new()) { + const pb::AllowNew& pb_allow_new = pb_entry.allow_new(); + + AllowNew allow_new; + if (pb_allow_new.has_source()) { + DeserializeSourceFromPb(pb_allow_new.source(), src_pool, &allow_new.source); + } + allow_new.comment = pb_allow_new.comment(); + entry->allow_new = std::move(allow_new); + } + + if (pb_entry.has_overlayable()) { + const pb::Overlayable& pb_overlayable = pb_entry.overlayable(); + + Overlayable overlayable; + if (pb_overlayable.has_source()) { + DeserializeSourceFromPb(pb_overlayable.source(), src_pool, &overlayable.source); + } + overlayable.comment = pb_overlayable.comment(); + entry->overlayable = std::move(overlayable); + } + ResourceId resid(pb_package.package_id().id(), pb_type.type_id().id(), pb_entry.entry_id().id()); if (resid.is_valid()) { diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp index 97ce01a9de13..78f12814389d 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -43,16 +43,16 @@ void SerializeSourceToPb(const Source& source, StringPool* src_pool, pb::Source* } } -static pb::SymbolStatus_Visibility SerializeVisibilityToPb(SymbolState state) { +static pb::Visibility::Level SerializeVisibilityToPb(Visibility::Level state) { switch (state) { - case SymbolState::kPrivate: - return pb::SymbolStatus_Visibility_PRIVATE; - case SymbolState::kPublic: - return pb::SymbolStatus_Visibility_PUBLIC; + case Visibility::Level::kPrivate: + return pb::Visibility::PRIVATE; + case Visibility::Level::kPublic: + return pb::Visibility::PUBLIC; default: break; } - return pb::SymbolStatus_Visibility_UNKNOWN; + return pb::Visibility::UNKNOWN; } void SerializeConfig(const ConfigDescription& config, pb::Configuration* out_pb_config) { @@ -293,12 +293,26 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table } pb_entry->set_name(entry->name); - // Write the SymbolStatus struct. - pb::SymbolStatus* pb_status = pb_entry->mutable_symbol_status(); - pb_status->set_visibility(SerializeVisibilityToPb(entry->symbol_status.state)); - SerializeSourceToPb(entry->symbol_status.source, &source_pool, pb_status->mutable_source()); - pb_status->set_comment(entry->symbol_status.comment); - pb_status->set_allow_new(entry->symbol_status.allow_new); + // Write the Visibility struct. + pb::Visibility* pb_visibility = pb_entry->mutable_visibility(); + pb_visibility->set_level(SerializeVisibilityToPb(entry->visibility.level)); + SerializeSourceToPb(entry->visibility.source, &source_pool, + pb_visibility->mutable_source()); + pb_visibility->set_comment(entry->visibility.comment); + + if (entry->allow_new) { + pb::AllowNew* pb_allow_new = pb_entry->mutable_allow_new(); + SerializeSourceToPb(entry->allow_new.value().source, &source_pool, + pb_allow_new->mutable_source()); + pb_allow_new->set_comment(entry->allow_new.value().comment); + } + + if (entry->overlayable) { + pb::Overlayable* pb_overlayable = pb_entry->mutable_overlayable(); + SerializeSourceToPb(entry->overlayable.value().source, &source_pool, + pb_overlayable->mutable_source()); + pb_overlayable->set_comment(entry->overlayable.value().comment); + } for (const std::unique_ptr<ResourceConfigValue>& config_value : entry->values) { pb::ConfigValue* pb_config_value = pb_entry->add_config_value(); diff --git a/tools/aapt2/format/proto/ProtoSerialize_test.cpp b/tools/aapt2/format/proto/ProtoSerialize_test.cpp index 9649a4de2fe4..d7f83fd9ecdc 100644 --- a/tools/aapt2/format/proto/ProtoSerialize_test.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize_test.cpp @@ -44,14 +44,15 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { .AddReference("com.app.a:layout/other", ResourceId(0x7f020001), "com.app.a:layout/main") .AddString("com.app.a:string/text", {}, "hi") .AddValue("com.app.a:id/foo", {}, util::make_unique<Id>()) - .SetSymbolState("com.app.a:bool/foo", {}, SymbolState::kUndefined, true /*allow_new*/) + .SetSymbolState("com.app.a:bool/foo", {}, Visibility::Level::kUndefined, + true /*allow_new*/) .Build(); - Symbol public_symbol; - public_symbol.state = SymbolState::kPublic; - ASSERT_TRUE(table->SetSymbolState(test::ParseNameOrDie("com.app.a:layout/main"), - ResourceId(0x7f020000), public_symbol, - context->GetDiagnostics())); + Visibility public_symbol; + public_symbol.level = Visibility::Level::kPublic; + ASSERT_TRUE(table->SetVisibilityWithId(test::ParseNameOrDie("com.app.a:layout/main"), + public_symbol, ResourceId(0x7f020000), + context->GetDiagnostics())); Id* id = test::GetValue<Id>(table.get(), "com.app.a:id/foo"); ASSERT_THAT(id, NotNull()); @@ -89,6 +90,10 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { test::ParseNameOrDie("com.app.a:layout/abc"), ConfigDescription::DefaultConfig(), {}, util::make_unique<Reference>(expected_ref), context->GetDiagnostics())); + // Make an overlayable resource. + ASSERT_TRUE(table->SetOverlayable(test::ParseNameOrDie("com.app.a:integer/overlayable"), + Overlayable{}, test::GetDiagnostics())); + pb::ResourceTable pb_table; SerializeTableToPb(*table, &pb_table); @@ -110,13 +115,13 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { new_table.FindResource(test::ParseNameOrDie("com.app.a:layout/main")); ASSERT_TRUE(result); - EXPECT_THAT(result.value().type->symbol_status.state, Eq(SymbolState::kPublic)); - EXPECT_THAT(result.value().entry->symbol_status.state, Eq(SymbolState::kPublic)); + EXPECT_THAT(result.value().type->visibility_level, Eq(Visibility::Level::kPublic)); + EXPECT_THAT(result.value().entry->visibility.level, Eq(Visibility::Level::kPublic)); result = new_table.FindResource(test::ParseNameOrDie("com.app.a:bool/foo")); ASSERT_TRUE(result); - EXPECT_THAT(result.value().entry->symbol_status.state, Eq(SymbolState::kUndefined)); - EXPECT_TRUE(result.value().entry->symbol_status.allow_new); + EXPECT_THAT(result.value().entry->visibility.level, Eq(Visibility::Level::kUndefined)); + EXPECT_TRUE(result.value().entry->allow_new); // Find the product-dependent values BinaryPrimitive* prim = test::GetValueForConfigAndProduct<BinaryPrimitive>( @@ -148,6 +153,12 @@ TEST(ProtoSerializeTest, SerializeSinglePackage) { EXPECT_THAT(*actual_styled_str->value->spans[0].name, Eq("b")); EXPECT_THAT(actual_styled_str->value->spans[0].first_char, Eq(0u)); EXPECT_THAT(actual_styled_str->value->spans[0].last_char, Eq(4u)); + + Maybe<ResourceTable::SearchResult> search_result = + new_table.FindResource(test::ParseNameOrDie("com.app.a:integer/overlayable")); + ASSERT_TRUE(search_result); + ASSERT_THAT(search_result.value().entry, NotNull()); + EXPECT_TRUE(search_result.value().entry->overlayable); } TEST(ProtoSerializeTest, SerializeAndDeserializeXml) { diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp index 8c8c2549609a..6b07b1e96261 100644 --- a/tools/aapt2/java/JavaClassGenerator.cpp +++ b/tools/aapt2/java/JavaClassGenerator.cpp @@ -191,14 +191,14 @@ JavaClassGenerator::JavaClassGenerator(IAaptContext* context, const JavaClassGeneratorOptions& options) : context_(context), table_(table), options_(options) {} -bool JavaClassGenerator::SkipSymbol(SymbolState state) { +bool JavaClassGenerator::SkipSymbol(Visibility::Level level) { switch (options_.types) { case JavaClassGeneratorOptions::SymbolTypes::kAll: return false; case JavaClassGeneratorOptions::SymbolTypes::kPublicPrivate: - return state == SymbolState::kUndefined; + return level == Visibility::Level::kUndefined; case JavaClassGeneratorOptions::SymbolTypes::kPublic: - return state != SymbolState::kPublic; + return level != Visibility::Level::kPublic; } return true; } @@ -444,8 +444,8 @@ void JavaClassGenerator::ProcessResource(const ResourceNameRef& name, const Reso AnnotationProcessor* processor = resource_member->GetCommentBuilder(); // Add the comments from any <public> tags. - if (entry.symbol_status.state != SymbolState::kUndefined) { - processor->AppendComment(entry.symbol_status.comment); + if (entry.visibility.level != Visibility::Level::kUndefined) { + processor->AppendComment(entry.visibility.comment); } // Add the comments from all configurations of this entry. @@ -484,7 +484,7 @@ void JavaClassGenerator::ProcessResource(const ResourceNameRef& name, const Reso Maybe<std::string> JavaClassGenerator::UnmangleResource(const StringPiece& package_name, const StringPiece& package_name_to_generate, const ResourceEntry& entry) { - if (SkipSymbol(entry.symbol_status.state)) { + if (SkipSymbol(entry.visibility.level)) { return {}; } diff --git a/tools/aapt2/java/JavaClassGenerator.h b/tools/aapt2/java/JavaClassGenerator.h index 4992f077c566..853120b3cb98 100644 --- a/tools/aapt2/java/JavaClassGenerator.h +++ b/tools/aapt2/java/JavaClassGenerator.h @@ -82,7 +82,7 @@ class JavaClassGenerator { static std::string TransformToFieldName(const android::StringPiece& symbol); private: - bool SkipSymbol(SymbolState state); + bool SkipSymbol(Visibility::Level state); bool SkipSymbol(const Maybe<SymbolTable::Symbol>& symbol); // Returns the unmangled resource entry name if the unmangled package is the same as diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp index 02f4cb14eb41..5beb594bd92f 100644 --- a/tools/aapt2/java/JavaClassGenerator_test.cpp +++ b/tools/aapt2/java/JavaClassGenerator_test.cpp @@ -139,8 +139,8 @@ TEST(JavaClassGeneratorTest, OnlyWritePublicResources) { .AddSimple("android:id/one", ResourceId(0x01020000)) .AddSimple("android:id/two", ResourceId(0x01020001)) .AddSimple("android:id/three", ResourceId(0x01020002)) - .SetSymbolState("android:id/one", ResourceId(0x01020000), SymbolState::kPublic) - .SetSymbolState("android:id/two", ResourceId(0x01020001), SymbolState::kPrivate) + .SetSymbolState("android:id/one", ResourceId(0x01020000), Visibility::Level::kPublic) + .SetSymbolState("android:id/two", ResourceId(0x01020001), Visibility::Level::kPrivate) .Build(); std::unique_ptr<IAaptContext> context = diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp index a68df1dbc998..da05dc328f7f 100644 --- a/tools/aapt2/link/ManifestFixer.cpp +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -430,7 +430,10 @@ bool ManifestFixer::Consume(IAaptContext* context, xml::XmlResource* doc) { return false; } - if (!executor.Execute(xml::XmlActionExecutorPolicy::kWhitelist, context->GetDiagnostics(), doc)) { + xml::XmlActionExecutorPolicy policy = options_.warn_validation + ? xml::XmlActionExecutorPolicy::kWhitelistWarning + : xml::XmlActionExecutorPolicy::kWhitelist; + if (!executor.Execute(policy, context->GetDiagnostics(), doc)) { return false; } diff --git a/tools/aapt2/link/ManifestFixer.h b/tools/aapt2/link/ManifestFixer.h index f5715f605b04..0caa52eaf650 100644 --- a/tools/aapt2/link/ManifestFixer.h +++ b/tools/aapt2/link/ManifestFixer.h @@ -57,6 +57,11 @@ struct ManifestFixerOptions { // The version codename of the framework being compiled against to set for // 'android:compileSdkVersionCodename' in the <manifest> tag. Maybe<std::string> compile_sdk_version_codename; + + // Wether validation errors should be treated only as warnings. If this is 'true', then an + // incorrect node will not result in an error, but only as a warning, and the parsing will + // continue. + bool warn_validation = false; }; // Verifies that the manifest is correctly formed and inserts defaults where specified with diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp index 1320dcd2a170..c6f895bb52dd 100644 --- a/tools/aapt2/link/ManifestFixer_test.cpp +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -112,7 +112,9 @@ TEST_F(ManifestFixerTest, AllowMetaData) { } TEST_F(ManifestFixerTest, UseDefaultSdkVersionsIfNonePresent) { - ManifestFixerOptions options = {std::string("8"), std::string("22")}; + ManifestFixerOptions options; + options.min_sdk_version_default = std::string("8"); + options.target_sdk_version_default = std::string("22"); std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" @@ -193,7 +195,9 @@ TEST_F(ManifestFixerTest, UseDefaultSdkVersionsIfNonePresent) { } TEST_F(ManifestFixerTest, UsesSdkMustComeBeforeApplication) { - ManifestFixerOptions options = {std::string("8"), std::string("22")}; + ManifestFixerOptions options; + options.min_sdk_version_default = std::string("8"); + options.target_sdk_version_default = std::string("22"); std::unique_ptr<xml::XmlResource> doc = VerifyWithOptions(R"EOF( <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> @@ -467,4 +471,27 @@ TEST_F(ManifestFixerTest, InsertCompileSdkVersions) { EXPECT_THAT(attr->value, StrEq("P")); } +TEST_F(ManifestFixerTest, UnexpectedElementsInManifest) { + std::string input = R"( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <beep/> + </manifest>)"; + ManifestFixerOptions options; + options.warn_validation = true; + + // Unexpected element should result in a warning if the flag is set to 'true'. + std::unique_ptr<xml::XmlResource> manifest = VerifyWithOptions(input, options); + ASSERT_THAT(manifest, NotNull()); + + // Unexpected element should result in an error if the flag is set to 'false'. + options.warn_validation = false; + manifest = VerifyWithOptions(input, options); + ASSERT_THAT(manifest, IsNull()); + + // By default the flag should be set to 'false'. + manifest = Verify(input); + ASSERT_THAT(manifest, IsNull()); +} + } // namespace aapt diff --git a/tools/aapt2/link/PrivateAttributeMover.cpp b/tools/aapt2/link/PrivateAttributeMover.cpp index eee4b60c1769..675b02a7e161 100644 --- a/tools/aapt2/link/PrivateAttributeMover.cpp +++ b/tools/aapt2/link/PrivateAttributeMover.cpp @@ -62,7 +62,7 @@ bool PrivateAttributeMover::Consume(IAaptContext* context, ResourceTable* table) continue; } - if (type->symbol_status.state != SymbolState::kPublic) { + if (type->visibility_level != Visibility::Level::kPublic) { // No public attributes, so we can safely leave these private attributes // where they are. continue; @@ -72,7 +72,7 @@ bool PrivateAttributeMover::Consume(IAaptContext* context, ResourceTable* table) move_if(type->entries, std::back_inserter(private_attr_entries), [](const std::unique_ptr<ResourceEntry>& entry) -> bool { - return entry->symbol_status.state != SymbolState::kPublic; + return entry->visibility.level != Visibility::Level::kPublic; }); if (private_attr_entries.empty()) { diff --git a/tools/aapt2/link/PrivateAttributeMover_test.cpp b/tools/aapt2/link/PrivateAttributeMover_test.cpp index 7fcf6e7ab5cd..168234b36e4a 100644 --- a/tools/aapt2/link/PrivateAttributeMover_test.cpp +++ b/tools/aapt2/link/PrivateAttributeMover_test.cpp @@ -30,9 +30,9 @@ TEST(PrivateAttributeMoverTest, MovePrivateAttributes) { .AddSimple("android:attr/publicB") .AddSimple("android:attr/privateB") .SetSymbolState("android:attr/publicA", ResourceId(0x01010000), - SymbolState::kPublic) + Visibility::Level::kPublic) .SetSymbolState("android:attr/publicB", ResourceId(0x01010000), - SymbolState::kPublic) + Visibility::Level::kPublic) .Build(); PrivateAttributeMover mover; @@ -81,7 +81,7 @@ TEST(PrivateAttributeMoverTest, DoNotCreatePrivateAttrsIfNoneExist) { std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() .AddSimple("android:attr/pub") - .SetSymbolState("android:attr/pub", ResourceId(0x01010000), SymbolState::kPublic) + .SetSymbolState("android:attr/pub", ResourceId(0x01010000), Visibility::Level::kPublic) .Build(); ResourceTablePackage* package = table->FindPackage("android"); diff --git a/tools/aapt2/link/ReferenceLinker.cpp b/tools/aapt2/link/ReferenceLinker.cpp index ad7d8b65350d..b8f880427c71 100644 --- a/tools/aapt2/link/ReferenceLinker.cpp +++ b/tools/aapt2/link/ReferenceLinker.cpp @@ -363,8 +363,8 @@ bool ReferenceLinker::Consume(IAaptContext* context, ResourceTable* table) { NameMangler::Unmangle(&name.entry, &name.package); // Symbol state information may be lost if there is no value for the resource. - if (entry->symbol_status.state != SymbolState::kUndefined && entry->values.empty()) { - context->GetDiagnostics()->Error(DiagMessage(entry->symbol_status.source) + if (entry->visibility.level != Visibility::Level::kUndefined && entry->values.empty()) { + context->GetDiagnostics()->Error(DiagMessage(entry->visibility.source) << "no definition for declared symbol '" << name << "'"); error = true; } diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp index 58d0607ed7b3..e819f51a5634 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -83,44 +83,58 @@ bool TableMerger::MergeAndMangle(const Source& src, const StringPiece& package_n static bool MergeType(IAaptContext* context, const Source& src, ResourceTableType* dst_type, ResourceTableType* src_type) { - if (dst_type->symbol_status.state < src_type->symbol_status.state) { + if (src_type->visibility_level > dst_type->visibility_level) { // The incoming type's visibility is stronger, so we should override the visibility. - if (src_type->symbol_status.state == SymbolState::kPublic) { + if (src_type->visibility_level == Visibility::Level::kPublic) { // Only copy the ID if the source is public, or else the ID is meaningless. dst_type->id = src_type->id; } - dst_type->symbol_status = std::move(src_type->symbol_status); - } else if (dst_type->symbol_status.state == SymbolState::kPublic && - src_type->symbol_status.state == SymbolState::kPublic && - dst_type->id && src_type->id && - dst_type->id.value() != src_type->id.value()) { + dst_type->visibility_level = src_type->visibility_level; + } else if (dst_type->visibility_level == Visibility::Level::kPublic && + src_type->visibility_level == Visibility::Level::kPublic && dst_type->id && + src_type->id && dst_type->id.value() != src_type->id.value()) { // Both types are public and have different IDs. - context->GetDiagnostics()->Error(DiagMessage(src) - << "cannot merge type '" << src_type->type - << "': conflicting public IDs"); + context->GetDiagnostics()->Error(DiagMessage(src) << "cannot merge type '" << src_type->type + << "': conflicting public IDs"); return false; } return true; } -static bool MergeEntry(IAaptContext* context, const Source& src, ResourceEntry* dst_entry, - ResourceEntry* src_entry) { - if (dst_entry->symbol_status.state < src_entry->symbol_status.state) { - // The incoming type's visibility is stronger, so we should override the visibility. - if (src_entry->symbol_status.state == SymbolState::kPublic) { - // Only copy the ID if the source is public, or else the ID is meaningless. +static bool MergeEntry(IAaptContext* context, const Source& src, bool overlay, + ResourceEntry* dst_entry, ResourceEntry* src_entry) { + // Copy over the strongest visibility. + if (src_entry->visibility.level > dst_entry->visibility.level) { + // Only copy the ID if the source is public, or else the ID is meaningless. + if (src_entry->visibility.level == Visibility::Level::kPublic) { dst_entry->id = src_entry->id; } - dst_entry->symbol_status = std::move(src_entry->symbol_status); - } else if (src_entry->symbol_status.state == SymbolState::kPublic && - dst_entry->symbol_status.state == SymbolState::kPublic && - dst_entry->id && src_entry->id && - dst_entry->id.value() != src_entry->id.value()) { + dst_entry->visibility = std::move(src_entry->visibility); + } else if (src_entry->visibility.level == Visibility::Level::kPublic && + dst_entry->visibility.level == Visibility::Level::kPublic && dst_entry->id && + src_entry->id && src_entry->id != dst_entry->id) { // Both entries are public and have different IDs. context->GetDiagnostics()->Error(DiagMessage(src) << "cannot merge entry '" << src_entry->name << "': conflicting public IDs"); return false; } + + // Copy over the rest of the properties, if needed. + if (src_entry->allow_new) { + dst_entry->allow_new = std::move(src_entry->allow_new); + } + + if (src_entry->overlayable) { + if (dst_entry->overlayable && !overlay) { + context->GetDiagnostics()->Error(DiagMessage(src_entry->overlayable.value().source) + << "duplicate overlayable declaration for resource '" + << src_entry->name << "'"); + context->GetDiagnostics()->Error(DiagMessage(dst_entry->overlayable.value().source) + << "previous declaration here"); + return false; + } + dst_entry->overlayable = std::move(src_entry->overlayable); + } return true; } @@ -202,7 +216,7 @@ bool TableMerger::DoMerge(const Source& src, ResourceTable* src_table, } ResourceEntry* dst_entry; - if (allow_new_resources || src_entry->symbol_status.allow_new) { + if (allow_new_resources || src_entry->allow_new) { dst_entry = dst_type->FindOrCreateEntry(entry_name); } else { dst_entry = dst_type->FindEntry(entry_name); @@ -220,7 +234,7 @@ bool TableMerger::DoMerge(const Source& src, ResourceTable* src_table, continue; } - if (!MergeEntry(context_, src, dst_entry, src_entry.get())) { + if (!MergeEntry(context_, src, overlay, dst_entry, src_entry.get())) { error = true; continue; } diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp index 6aab8ded24a5..34461c6b467d 100644 --- a/tools/aapt2/link/TableMerger_test.cpp +++ b/tools/aapt2/link/TableMerger_test.cpp @@ -182,14 +182,12 @@ TEST_F(TableMergerTest, OverrideSameResourceIdsWithOverlay) { std::unique_ptr<ResourceTable> base = test::ResourceTableBuilder() .SetPackageId("", 0x7f) - .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), - SymbolState::kPublic) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) .Build(); std::unique_ptr<ResourceTable> overlay = test::ResourceTableBuilder() .SetPackageId("", 0x7f) - .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), - SymbolState::kPublic) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) .Build(); ResourceTable final_table; @@ -205,14 +203,12 @@ TEST_F(TableMergerTest, FailToOverrideConflictingTypeIdsWithOverlay) { std::unique_ptr<ResourceTable> base = test::ResourceTableBuilder() .SetPackageId("", 0x7f) - .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), - SymbolState::kPublic) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) .Build(); std::unique_ptr<ResourceTable> overlay = test::ResourceTableBuilder() .SetPackageId("", 0x7f) - .SetSymbolState("bool/foo", ResourceId(0x7f, 0x02, 0x0001), - SymbolState::kPublic) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x02, 0x0001), Visibility::Level::kPublic) .Build(); ResourceTable final_table; @@ -228,14 +224,12 @@ TEST_F(TableMergerTest, FailToOverrideConflictingEntryIdsWithOverlay) { std::unique_ptr<ResourceTable> base = test::ResourceTableBuilder() .SetPackageId("", 0x7f) - .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), - SymbolState::kPublic) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0001), Visibility::Level::kPublic) .Build(); std::unique_ptr<ResourceTable> overlay = test::ResourceTableBuilder() .SetPackageId("", 0x7f) - .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0002), - SymbolState::kPublic) + .SetSymbolState("bool/foo", ResourceId(0x7f, 0x01, 0x0002), Visibility::Level::kPublic) .Build(); ResourceTable final_table; @@ -253,7 +247,7 @@ TEST_F(TableMergerTest, MergeAddResourceFromOverlay) { std::unique_ptr<ResourceTable> table_b = test::ResourceTableBuilder() .SetPackageId("", 0x7f) - .SetSymbolState("bool/foo", {}, SymbolState::kUndefined, true /*allow new overlay*/) + .SetSymbolState("bool/foo", {}, Visibility::Level::kUndefined, true /*allow new overlay*/) .AddValue("bool/foo", ResourceUtils::TryParseBool("true")) .Build(); diff --git a/tools/aapt2/optimize/MultiApkGenerator.cpp b/tools/aapt2/optimize/MultiApkGenerator.cpp index 16898d6f3e03..991faadcafc4 100644 --- a/tools/aapt2/optimize/MultiApkGenerator.cpp +++ b/tools/aapt2/optimize/MultiApkGenerator.cpp @@ -120,8 +120,6 @@ MultiApkGenerator::MultiApkGenerator(LoadedApk* apk, IAaptContext* context) } bool MultiApkGenerator::FromBaseApk(const MultiApkGeneratorOptions& options) { - // TODO(safarmer): Handle APK version codes for the generated APKs. - std::unordered_set<std::string> artifacts_to_keep = options.kept_artifacts; std::unordered_set<std::string> filtered_artifacts; std::unordered_set<std::string> kept_artifacts; @@ -237,8 +235,8 @@ std::unique_ptr<ResourceTable> MultiApkGenerator::FilterTable(IAaptContext* cont splits.config_filter = &axis_filter; } - if (artifact.android_sdk && artifact.android_sdk.value().min_sdk_version) { - wrapped_context.SetMinSdkVersion(artifact.android_sdk.value().min_sdk_version.value()); + if (artifact.android_sdk) { + wrapped_context.SetMinSdkVersion(artifact.android_sdk.value().min_sdk_version); } std::unique_ptr<ResourceTable> table = old_table.Clone(); @@ -301,7 +299,7 @@ bool MultiApkGenerator::UpdateManifest(const OutputArtifact& artifact, if (xml::Attribute* min_sdk_attr = uses_sdk_el->FindAttribute(xml::kSchemaAndroid, "minSdkVersion")) { // Populate with a pre-compiles attribute to we don't need to relink etc. - const std::string& min_sdk_str = std::to_string(android_sdk.min_sdk_version.value()); + const std::string& min_sdk_str = std::to_string(android_sdk.min_sdk_version); min_sdk_attr->compiled_value = ResourceUtils::TryParseInt(min_sdk_str); } else { // There was no minSdkVersion. This is strange since at this point we should have been diff --git a/tools/aapt2/process/SymbolTable.cpp b/tools/aapt2/process/SymbolTable.cpp index 0cfc0bdfaaa6..3cae0e8ae462 100644 --- a/tools/aapt2/process/SymbolTable.cpp +++ b/tools/aapt2/process/SymbolTable.cpp @@ -190,7 +190,7 @@ std::unique_ptr<SymbolTable::Symbol> ResourceTableSymbolSource::FindByName( ResourceTable::SearchResult sr = result.value(); std::unique_ptr<SymbolTable::Symbol> symbol = util::make_unique<SymbolTable::Symbol>(); - symbol->is_public = (sr.entry->symbol_status.state == SymbolState::kPublic); + symbol->is_public = (sr.entry->visibility.level == Visibility::Level::kPublic); if (sr.package->id && sr.type->id && sr.entry->id) { symbol->id = ResourceId(sr.package->id.value(), sr.type->id.value(), sr.entry->id.value()); diff --git a/tools/aapt2/split/TableSplitter.cpp b/tools/aapt2/split/TableSplitter.cpp index 9d49ca6c0aa9..e99174302d26 100644 --- a/tools/aapt2/split/TableSplitter.cpp +++ b/tools/aapt2/split/TableSplitter.cpp @@ -233,13 +233,13 @@ void TableSplitter::SplitTable(ResourceTable* original_table) { ResourceTableType* split_type = split_pkg->FindOrCreateType(type->type); if (!split_type->id) { split_type->id = type->id; - split_type->symbol_status = type->symbol_status; + split_type->visibility_level = type->visibility_level; } ResourceEntry* split_entry = split_type->FindOrCreateEntry(entry->name); if (!split_entry->id) { split_entry->id = entry->id; - split_entry->symbol_status = entry->symbol_status; + split_entry->visibility = entry->visibility; } // Copy the selected values into the new Split Entry. diff --git a/tools/aapt2/test/Builders.cpp b/tools/aapt2/test/Builders.cpp index 88897a8afac3..495a48a830f7 100644 --- a/tools/aapt2/test/Builders.cpp +++ b/tools/aapt2/test/Builders.cpp @@ -26,6 +26,7 @@ using ::aapt::configuration::Abi; using ::aapt::configuration::AndroidSdk; using ::aapt::configuration::ConfiguredArtifact; +using ::aapt::configuration::GetOrCreateGroup; using ::aapt::io::StringInputStream; using ::android::StringPiece; @@ -116,19 +117,20 @@ ResourceTableBuilder& ResourceTableBuilder::AddValue(const StringPiece& name, const ResourceId& id, std::unique_ptr<Value> value) { ResourceName res_name = ParseNameOrDie(name); - CHECK(table_->AddResourceAllowMangled(res_name, id, config, {}, std::move(value), - GetDiagnostics())); + CHECK(table_->AddResourceWithIdMangled(res_name, id, config, {}, std::move(value), + GetDiagnostics())); return *this; } ResourceTableBuilder& ResourceTableBuilder::SetSymbolState(const StringPiece& name, - const ResourceId& id, SymbolState state, + const ResourceId& id, + Visibility::Level level, bool allow_new) { ResourceName res_name = ParseNameOrDie(name); - Symbol symbol; - symbol.state = state; - symbol.allow_new = allow_new; - CHECK(table_->SetSymbolStateAllowMangled(res_name, id, symbol, GetDiagnostics())); + Visibility visibility; + visibility.level = level; + CHECK(table_->SetVisibilityWithIdMangled(res_name, visibility, id, GetDiagnostics())); + CHECK(table_->SetAllowNewMangled(res_name, AllowNew{}, GetDiagnostics())); return *this; } @@ -226,6 +228,11 @@ ArtifactBuilder& ArtifactBuilder::SetName(const std::string& name) { return *this; } +ArtifactBuilder& ArtifactBuilder::SetVersion(int version) { + artifact_.version = version; + return *this; +} + ArtifactBuilder& ArtifactBuilder::AddAbi(configuration::Abi abi) { artifact_.abis.push_back(abi); return *this; @@ -250,5 +257,54 @@ configuration::OutputArtifact ArtifactBuilder::Build() { return artifact_; } +PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::AddAbiGroup( + const std::string& label, std::vector<configuration::Abi> abis) { + return AddGroup(label, &config_.abi_groups, std::move(abis)); +} + +PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::AddDensityGroup( + const std::string& label, std::vector<std::string> densities) { + std::vector<ConfigDescription> configs; + for (const auto& density : densities) { + configs.push_back(test::ParseConfigOrDie(density)); + } + return AddGroup(label, &config_.screen_density_groups, configs); +} + +PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::AddLocaleGroup( + const std::string& label, std::vector<std::string> locales) { + std::vector<ConfigDescription> configs; + for (const auto& locale : locales) { + configs.push_back(test::ParseConfigOrDie(locale)); + } + return AddGroup(label, &config_.locale_groups, configs); +} + +PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::AddDeviceFeatureGroup( + const std::string& label) { + return AddGroup(label, &config_.device_feature_groups); +} + +PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::AddGlTextureGroup( + const std::string& label) { + return AddGroup(label, &config_.gl_texture_groups); +} + +PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::AddAndroidSdk( + std::string label, int min_sdk) { + config_.android_sdks[label] = AndroidSdk::ForMinSdk(min_sdk); + return *this; +} + +PostProcessingConfigurationBuilder& PostProcessingConfigurationBuilder::AddArtifact( + configuration::ConfiguredArtifact artifact) { + config_.artifacts.push_back(std::move(artifact)); + return *this; +} + +configuration::PostProcessingConfiguration PostProcessingConfigurationBuilder::Build() { + return config_; +} + } // namespace test } // namespace aapt diff --git a/tools/aapt2/test/Builders.h b/tools/aapt2/test/Builders.h index 2f83b78ffb2a..0d7451b5a6da 100644 --- a/tools/aapt2/test/Builders.h +++ b/tools/aapt2/test/Builders.h @@ -68,7 +68,7 @@ class ResourceTableBuilder { ResourceTableBuilder& AddValue(const android::StringPiece& name, const ConfigDescription& config, const ResourceId& id, std::unique_ptr<Value> value); ResourceTableBuilder& SetSymbolState(const android::StringPiece& name, const ResourceId& id, - SymbolState state, bool allow_new = false); + Visibility::Level level, bool allow_new = false); StringPool* string_pool(); std::unique_ptr<ResourceTable> Build(); @@ -160,6 +160,7 @@ class ArtifactBuilder { ArtifactBuilder() = default; ArtifactBuilder& SetName(const std::string& name); + ArtifactBuilder& SetVersion(int version); ArtifactBuilder& AddAbi(configuration::Abi abi); ArtifactBuilder& AddDensity(const ConfigDescription& density); ArtifactBuilder& AddLocale(const ConfigDescription& locale); @@ -167,9 +168,41 @@ class ArtifactBuilder { configuration::OutputArtifact Build(); private: + DISALLOW_COPY_AND_ASSIGN(ArtifactBuilder); + configuration::OutputArtifact artifact_; }; +class PostProcessingConfigurationBuilder { + public: + PostProcessingConfigurationBuilder() = default; + + PostProcessingConfigurationBuilder& AddAbiGroup(const std::string& label, + std::vector<configuration::Abi> abis = {}); + PostProcessingConfigurationBuilder& AddDensityGroup(const std::string& label, + std::vector<std::string> densities = {}); + PostProcessingConfigurationBuilder& AddLocaleGroup(const std::string& label, + std::vector<std::string> locales = {}); + PostProcessingConfigurationBuilder& AddDeviceFeatureGroup(const std::string& label); + PostProcessingConfigurationBuilder& AddGlTextureGroup(const std::string& label); + PostProcessingConfigurationBuilder& AddAndroidSdk(std::string label, int min_sdk); + PostProcessingConfigurationBuilder& AddArtifact(configuration::ConfiguredArtifact artrifact); + + configuration::PostProcessingConfiguration Build(); + + private: + template <typename T> + inline PostProcessingConfigurationBuilder& AddGroup(const std::string& label, + configuration::Group<T>* group, + std::vector<T> to_add = {}) { + auto& values = GetOrCreateGroup(label, group); + values.insert(std::begin(values), std::begin(to_add), std::end(to_add)); + return *this; + } + + configuration::PostProcessingConfiguration config_; +}; + } // namespace test } // namespace aapt diff --git a/tools/aapt2/xml/XmlActionExecutor.cpp b/tools/aapt2/xml/XmlActionExecutor.cpp index 602a902bfc3e..cb844f085ecc 100644 --- a/tools/aapt2/xml/XmlActionExecutor.cpp +++ b/tools/aapt2/xml/XmlActionExecutor.cpp @@ -66,7 +66,7 @@ bool XmlNodeAction::Execute(XmlActionExecutorPolicy policy, std::vector<StringPi continue; } - if (policy == XmlActionExecutorPolicy::kWhitelist) { + if (policy != XmlActionExecutorPolicy::kNone) { DiagMessage error_msg(child_el->line_number); error_msg << "unexpected element "; PrintElementToDiagMessage(child_el, &error_msg); @@ -74,8 +74,14 @@ bool XmlNodeAction::Execute(XmlActionExecutorPolicy policy, std::vector<StringPi for (const StringPiece& element : *bread_crumb) { error_msg << "<" << element << ">"; } - diag->Error(error_msg); - error = true; + if (policy == XmlActionExecutorPolicy::kWhitelistWarning) { + // Treat the error only as a warning. + diag->Warn(error_msg); + } else { + // Policy is XmlActionExecutorPolicy::kWhitelist, we should fail. + diag->Error(error_msg); + error = true; + } } } } diff --git a/tools/aapt2/xml/XmlActionExecutor.h b/tools/aapt2/xml/XmlActionExecutor.h index df70100e882a..f689b2a3eaa8 100644 --- a/tools/aapt2/xml/XmlActionExecutor.h +++ b/tools/aapt2/xml/XmlActionExecutor.h @@ -34,10 +34,15 @@ enum class XmlActionExecutorPolicy { // Actions are run if elements are matched, errors occur only when actions return false. kNone, - // The actions defined must match and run. If an element is found that does - // not match an action, an error occurs. + // The actions defined must match and run. If an element is found that does not match an action, + // an error occurs. // Note: namespaced elements are always ignored. kWhitelist, + + // The actions defined should match and run. if an element is found that does not match an + // action, a warning is printed. + // Note: namespaced elements are always ignored. + kWhitelistWarning, }; // Contains the actions to perform at this XML node. This is a recursive data structure that |