Only autoVerify at install for new hosts

Re-run app link verification at update time only when the set of hosts
has expanded.  Intentionally revoke verify history when an app stops
using autoVerify, as a one-time measure to place it back into the
non-autoverify model for tracking the user's launch preferences.  If the
app starts using autoVerify again later, it behaves identically to an
app that has never done so before.

In the included AutoVerify test apks (all the same package name):

  app1 recognizes 1 host
  app2 recognizes the same host plus a different wildcard
  app3 has the same hosts as app1 but does not autoVerify
  app4 has the same hosts as app2 but does not autoVerify

Bug: 151475497
Test: install AutoVerify app1, set to 'always', install app2, observe
that it is demoted out of always

Test: install app1 apk, set 'always', install app1 again, observe that
no verification run is performed

Test: install app2 apk, set 'always', install app1, observe that
no verification run is performed

Test: install app1 apk, verify still undefined/ask, install app1 again,
observe that a verification is performed

Test: install app2 apk, set 'always', install app1, observe that
no verification run is performed, install app2 again, observe that it is
reverified (and demoted out of always when verification fails)

Test: install app1 apk, set 'always', install app3 apk, observe that no
verification is performed & it remains in always

Test: install app1 apk, set 'always', install app4 apk, observe that no
verification is performed but it is demoted out of always

Change-Id: I200d85085ce79842a3ed39377d1f75ec381c8991
This commit is contained in:
Christopher Tate
2020-03-19 15:24:56 -07:00
parent c7cd73aa00
commit 55b676bc4f
18 changed files with 454 additions and 36 deletions

View File

@@ -1346,13 +1346,6 @@ public class PackageManagerService extends IPackageManager.Stub
int updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; int updatedStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED;
boolean needUpdate = false; boolean needUpdate = false;
if (DEBUG_DOMAIN_VERIFICATION) {
Slog.d(TAG,
"Updating IntentFilterVerificationInfo for package " + packageName
+ " verificationId:" + verificationId
+ " verified=" + verified);
}
// In a success case, we promote from undefined or ASK to ALWAYS. This // In a success case, we promote from undefined or ASK to ALWAYS. This
// supports a flow where the app fails validation but then ships an updated // supports a flow where the app fails validation but then ships an updated
// APK that passes, and therefore deserves to be in ALWAYS. // APK that passes, and therefore deserves to be in ALWAYS.
@@ -17632,65 +17625,120 @@ public class PackageManagerService extends IPackageManager.Stub
+ " Activities needs verification ..."); + " Activities needs verification ...");
int count = 0; int count = 0;
boolean handlesWebUris = false;
ArraySet<String> domains = new ArraySet<>();
final boolean previouslyVerified;
boolean hostSetExpanded = false;
boolean needToRunVerify = false;
synchronized (mLock) { synchronized (mLock) {
// If this is a new install and we see that we've already run verification for this // If this is a new install and we see that we've already run verification for this
// package, we have nothing to do: it means the state was restored from backup. // package, we have nothing to do: it means the state was restored from backup.
if (!replacing) { IntentFilterVerificationInfo ivi =
IntentFilterVerificationInfo ivi = mSettings.getIntentFilterVerificationLPr(packageName);
mSettings.getIntentFilterVerificationLPr(packageName); previouslyVerified = (ivi != null);
if (ivi != null) { if (!replacing && previouslyVerified) {
if (DEBUG_DOMAIN_VERIFICATION) { if (DEBUG_DOMAIN_VERIFICATION) {
Slog.i(TAG, "Package " + packageName+ " already verified: status=" Slog.i(TAG, "Package " + packageName + " already verified: status="
+ ivi.getStatusString()); + ivi.getStatusString());
}
return;
} }
return;
} }
// If any filters need to be verified, then all need to be. if (DEBUG_DOMAIN_VERIFICATION) {
boolean needToVerify = false; Slog.i(TAG, " Previous verified hosts: "
+ (ivi == null ? "[none]" : ivi.getDomainsString()));
}
// If any filters need to be verified, then all need to be. In addition, we need to
// know whether an updating app has any web navigation intent filters, to re-
// examine handling policy even if not re-verifying.
final boolean needsVerification = needsNetworkVerificationLPr(packageName);
for (ParsedActivity a : activities) { for (ParsedActivity a : activities) {
for (ParsedIntentInfo filter : a.getIntents()) { for (ParsedIntentInfo filter : a.getIntents()) {
if (filter.needsVerification() if (filter.handlesWebUris(true)) {
&& needsNetworkVerificationLPr(a.getPackageName())) { handlesWebUris = true;
}
if (needsVerification && filter.needsVerification()) {
if (DEBUG_DOMAIN_VERIFICATION) { if (DEBUG_DOMAIN_VERIFICATION) {
Slog.d(TAG, Slog.d(TAG, "autoVerify requested, processing all filters");
"Intent filter needs verification, so processing all filters");
} }
needToVerify = true; needToRunVerify = true;
// It's safe to break out here because filter.needsVerification()
// can only be true if filter.handlesWebUris(true) returned true, so
// we've already noted that.
break; break;
} }
} }
} }
if (needToVerify) { // Compare the new set of recognized hosts if the app is either requesting
final boolean needsVerification = needsNetworkVerificationLPr(packageName); // autoVerify or has previously used autoVerify but no longer does.
if (needToRunVerify || previouslyVerified) {
final int verificationId = mIntentFilterVerificationToken++; final int verificationId = mIntentFilterVerificationToken++;
for (ParsedActivity a : activities) { for (ParsedActivity a : activities) {
for (ParsedIntentInfo filter : a.getIntents()) { for (ParsedIntentInfo filter : a.getIntents()) {
// Run verification against hosts mentioned in any web-nav intent filter, // Run verification against hosts mentioned in any web-nav intent filter,
// even if the filter matches non-web schemes as well // even if the filter matches non-web schemes as well
if (needsVerification && filter.handlesWebUris(false)) { if (filter.handlesWebUris(false /*onlyWebSchemes*/)) {
if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG, if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG,
"Verification needed for IntentFilter:" + filter.toString()); "Verification needed for IntentFilter:" + filter.toString());
mIntentFilterVerifier.addOneIntentFilterVerification( mIntentFilterVerifier.addOneIntentFilterVerification(
verifierUid, userId, verificationId, filter, packageName); verifierUid, userId, verificationId, filter, packageName);
domains.addAll(filter.getHostsList());
count++; count++;
} }
} }
} }
} }
if (DEBUG_DOMAIN_VERIFICATION) {
Slog.i(TAG, " Update published hosts: " + domains.toString());
}
// If we've previously verified this same host set (or a subset), we can trust that
// a current ALWAYS policy is still applicable. If this is the case, we're done.
// (If we aren't in ALWAYS, we want to reverify to allow for apps that had failing
// hosts in their intent filters, then pushed a new apk that removed them and now
// passes.)
//
// Cases:
// + still autoVerify (needToRunVerify):
// - preserve current state if all of: unexpanded, in always
// - otherwise rerun as usual (fall through)
// + no longer autoVerify (alreadyVerified && !needToRunVerify)
// - wipe verification history always
// - preserve current state if all of: unexpanded, in always
hostSetExpanded = !previouslyVerified
|| (ivi != null && !ivi.getDomains().containsAll(domains));
final int currentPolicy =
mSettings.getIntentFilterVerificationStatusLPr(packageName, userId);
final boolean keepCurState = !hostSetExpanded
&& currentPolicy == INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS;
if (needToRunVerify && keepCurState) {
if (DEBUG_DOMAIN_VERIFICATION) {
Slog.i(TAG, "Host set not expanding + ALWAYS -> no need to reverify");
}
ivi.setDomains(domains);
scheduleWriteSettingsLocked();
return;
} else if (previouslyVerified && !needToRunVerify) {
// Prior autoVerify state but not requesting it now. Clear autoVerify history,
// and preserve the always policy iff the host set is not expanding.
clearIntentFilterVerificationsLPw(packageName, userId, !keepCurState);
return;
}
} }
if (count > 0) { if (needToRunVerify && count > 0) {
// app requested autoVerify and has at least one matching intent filter
if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG, "Starting " + count if (DEBUG_DOMAIN_VERIFICATION) Slog.d(TAG, "Starting " + count
+ " IntentFilter verification" + (count > 1 ? "s" : "") + " IntentFilter verification" + (count > 1 ? "s" : "")
+ " for userId:" + userId); + " for userId:" + userId);
mIntentFilterVerifier.startVerifications(userId); mIntentFilterVerifier.startVerifications(userId);
} else { } else {
if (DEBUG_DOMAIN_VERIFICATION) { if (DEBUG_DOMAIN_VERIFICATION) {
Slog.d(TAG, "No filters or not all autoVerify for " + packageName); Slog.d(TAG, "No web filters or no new host policy for " + packageName);
} }
} }
} }
@@ -18395,7 +18443,7 @@ public class PackageManagerService extends IPackageManager.Stub
if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) { if ((flags & PackageManager.DELETE_KEEP_DATA) == 0) {
final SparseBooleanArray changedUsers = new SparseBooleanArray(); final SparseBooleanArray changedUsers = new SparseBooleanArray();
synchronized (mLock) { synchronized (mLock) {
clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL); clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL, true);
clearDefaultBrowserIfNeeded(packageName); clearDefaultBrowserIfNeeded(packageName);
mSettings.mKeySetManagerService.removeAppKeySetDataLPw(packageName); mSettings.mKeySetManagerService.removeAppKeySetDataLPw(packageName);
removedAppId = mSettings.removePackageLPw(packageName); removedAppId = mSettings.removePackageLPw(packageName);
@@ -19488,13 +19536,14 @@ public class PackageManagerService extends IPackageManager.Stub
final int packageCount = mPackages.size(); final int packageCount = mPackages.size();
for (int i = 0; i < packageCount; i++) { for (int i = 0; i < packageCount; i++) {
AndroidPackage pkg = mPackages.valueAt(i); AndroidPackage pkg = mPackages.valueAt(i);
clearIntentFilterVerificationsLPw(pkg.getPackageName(), userId); clearIntentFilterVerificationsLPw(pkg.getPackageName(), userId, true);
} }
} }
/** This method takes a specific user id as well as UserHandle.USER_ALL. */ /** This method takes a specific user id as well as UserHandle.USER_ALL. */
@GuardedBy("mLock") @GuardedBy("mLock")
void clearIntentFilterVerificationsLPw(String packageName, int userId) { void clearIntentFilterVerificationsLPw(String packageName, int userId,
boolean alsoResetStatus) {
if (userId == UserHandle.USER_ALL) { if (userId == UserHandle.USER_ALL) {
if (mSettings.removeIntentFilterVerificationLPw(packageName, if (mSettings.removeIntentFilterVerificationLPw(packageName,
mUserManager.getUserIds())) { mUserManager.getUserIds())) {
@@ -19503,7 +19552,8 @@ public class PackageManagerService extends IPackageManager.Stub
} }
} }
} else { } else {
if (mSettings.removeIntentFilterVerificationLPw(packageName, userId)) { if (mSettings.removeIntentFilterVerificationLPw(packageName, userId,
alsoResetStatus)) {
scheduleWritePackageRestrictionsLocked(userId); scheduleWritePackageRestrictionsLocked(userId);
} }
} }

View File

@@ -1282,7 +1282,8 @@ public final class Settings {
return result; return result;
} }
boolean removeIntentFilterVerificationLPw(String packageName, int userId) { boolean removeIntentFilterVerificationLPw(String packageName, int userId,
boolean alsoResetStatus) {
PackageSetting ps = mPackages.get(packageName); PackageSetting ps = mPackages.get(packageName);
if (ps == null) { if (ps == null) {
if (DEBUG_DOMAIN_VERIFICATION) { if (DEBUG_DOMAIN_VERIFICATION) {
@@ -1290,14 +1291,17 @@ public final class Settings {
} }
return false; return false;
} }
ps.clearDomainVerificationStatusForUser(userId); if (alsoResetStatus) {
ps.clearDomainVerificationStatusForUser(userId);
}
ps.setIntentFilterVerificationInfo(null);
return true; return true;
} }
boolean removeIntentFilterVerificationLPw(String packageName, int[] userIds) { boolean removeIntentFilterVerificationLPw(String packageName, int[] userIds) {
boolean result = false; boolean result = false;
for (int userId : userIds) { for (int userId : userIds) {
result |= removeIntentFilterVerificationLPw(packageName, userId); result |= removeIntentFilterVerificationLPw(packageName, userId, true);
} }
return result; return result;
} }

View File

@@ -0,0 +1,11 @@
android_app {
name: "AutoVerifyTest",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
platform_apis: true,
min_sdk_version: "26",
target_sdk_version: "26",
optimize: {
enabled: false,
},
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.test.autoverify" >
<uses-sdk android:targetSdkVersion="26" />
<application
android:label="@string/app_name" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="explicit.example.com" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- app icon label, do not translate -->
<string name="app_name" translatable="false">AutoVerify Test</string>
</resources>

View File

@@ -0,0 +1,15 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@@ -0,0 +1,11 @@
android_app {
name: "AutoVerifyTest2",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
platform_apis: true,
min_sdk_version: "26",
target_sdk_version: "26",
optimize: {
enabled: false,
},
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.test.autoverify" >
<uses-sdk android:targetSdkVersion="26" />
<application
android:label="@string/app_name" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="explicit.example.com" />
<data android:host="*.wildcard.tld" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- app icon label, do not translate -->
<string name="app_name" translatable="false">AutoVerify Test</string>
</resources>

View File

@@ -0,0 +1,15 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@@ -0,0 +1,11 @@
android_app {
name: "AutoVerifyTest3",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
platform_apis: true,
min_sdk_version: "26",
target_sdk_version: "26",
optimize: {
enabled: false,
},
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.test.autoverify" >
<uses-sdk android:targetSdkVersion="26" />
<application
android:label="@string/app_name" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- does not request autoVerify -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="explicit.example.com" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- app icon label, do not translate -->
<string name="app_name" translatable="false">AutoVerify Test</string>
</resources>

View File

@@ -0,0 +1,15 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

View File

@@ -0,0 +1,11 @@
android_app {
name: "AutoVerifyTest4",
srcs: ["src/**/*.java"],
resource_dirs: ["res"],
platform_apis: true,
min_sdk_version: "26",
target_sdk_version: "26",
optimize: {
enabled: false,
},
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.test.autoverify" >
<uses-sdk android:targetSdkVersion="26" />
<application
android:label="@string/app_name" >
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- intentionally does not autoVerify -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="explicit.example.com" />
<data android:host="*.wildcard.tld" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2020 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- app icon label, do not translate -->
<string name="app_name" translatable="false">AutoVerify Test</string>
</resources>

View File

@@ -0,0 +1,15 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/