Merge "Constrain 'touch modal' behavior to the activity stack."
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 81b1583..ed05321 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -94,7 +94,7 @@
import android.view.Window;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
-import android.renderscript.RenderScript;
+import android.renderscript.RenderScriptCacheDir;
import android.security.AndroidKeyStoreProvider;
import com.android.internal.app.IVoiceInteractor;
@@ -3214,7 +3214,7 @@
if (cv == null) {
mThumbnailCanvas = cv = new Canvas();
}
-
+
cv.setBitmap(thumbnail);
if (!r.activity.onCreateThumbnail(thumbnail, cv)) {
mAvailThumbnailBitmap = thumbnail;
@@ -3516,12 +3516,12 @@
private void handleWindowVisibility(IBinder token, boolean show) {
ActivityClientRecord r = mActivities.get(token);
-
+
if (r == null) {
Log.w(TAG, "handleWindowVisibility: no activity for token " + token);
return;
}
-
+
if (!show && !r.stopped) {
performStopActivityInner(r, null, show, false);
} else if (show && r.stopped) {
@@ -3953,10 +3953,10 @@
}
}
}
-
+
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Relaunching activity "
+ tmp.token + ": changedConfig=" + changedConfig);
-
+
// If there was a pending configuration change, execute it first.
if (changedConfig != null) {
mCurDefaultDisplayDpi = changedConfig.densityDpi;
@@ -4154,7 +4154,7 @@
if (config == null) {
return;
}
-
+
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle configuration changed: "
+ config);
@@ -4292,7 +4292,7 @@
ApplicationPackageManager.handlePackageBroadcast(cmd, packages,
hasPkgInfo);
}
-
+
final void handleLowMemory() {
ArrayList<ComponentCallbacks2> callbacks = collectComponentCallbacks(true, null);
@@ -4339,10 +4339,10 @@
String[] packages = getPackageManager().getPackagesForUid(uid);
// If there are several packages in this application we won't
- // initialize the graphics disk caches
+ // initialize the graphics disk caches
if (packages != null && packages.length == 1) {
HardwareRenderer.setupDiskCache(cacheDir);
- RenderScript.setupDiskCache(cacheDir);
+ RenderScriptCacheDir.setupDiskCache(cacheDir);
}
} catch (RemoteException e) {
// Ignore
@@ -5267,7 +5267,7 @@
if (mPendingConfiguration == null ||
mPendingConfiguration.isOtherSeqNewer(newConfig)) {
mPendingConfiguration = newConfig;
-
+
sendMessage(H.CONFIGURATION_CHANGED, newConfig);
}
}
diff --git a/core/java/android/inputmethodservice/ExtractEditLayout.java b/core/java/android/inputmethodservice/ExtractEditLayout.java
index 5696839..e902443 100644
--- a/core/java/android/inputmethodservice/ExtractEditLayout.java
+++ b/core/java/android/inputmethodservice/ExtractEditLayout.java
@@ -163,6 +163,8 @@
mCallback.onDestroyActionMode(this);
mCallback = null;
+ mMenu.close();
+
mExtractActionButton.setVisibility(VISIBLE);
mEditButton.setVisibility(INVISIBLE);
diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java
index 16353e8..205d35e 100644
--- a/core/java/android/widget/ProgressBar.java
+++ b/core/java/android/widget/ProgressBar.java
@@ -1810,9 +1810,7 @@
}
if (mRefreshProgressRunnable != null) {
removeCallbacks(mRefreshProgressRunnable);
- }
- if (mRefreshProgressRunnable != null && mRefreshIsPosted) {
- removeCallbacks(mRefreshProgressRunnable);
+ mRefreshIsPosted = false;
}
if (mAccessibilityEventSender != null) {
removeCallbacks(mAccessibilityEventSender);
diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java
index 52e1728..9571109 100644
--- a/core/java/android/widget/RadialTimePickerView.java
+++ b/core/java/android/widget/RadialTimePickerView.java
@@ -79,10 +79,10 @@
// Transparent alpha level
private static final int ALPHA_TRANSPARENT = 0;
- private static final int HOURS_IN_DAY = 24;
- private static final int MINUTES_IN_HOUR = 60;
- private static final int DEGREES_FOR_ONE_HOUR = 360 / HOURS_IN_DAY;
- private static final int DEGREES_FOR_ONE_MINUTE = 360 / MINUTES_IN_HOUR;
+ private static final int HOURS_IN_CIRCLE = 12;
+ private static final int MINUTES_IN_CIRCLE = 60;
+ private static final int DEGREES_FOR_ONE_HOUR = 360 / HOURS_IN_CIRCLE;
+ private static final int DEGREES_FOR_ONE_MINUTE = 360 / MINUTES_IN_CIRCLE;
private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
@@ -536,7 +536,7 @@
}
private void setCurrentMinuteInternal(int minute, boolean callback) {
- mSelectionDegrees[MINUTES] = (minute % MINUTES_IN_HOUR) * DEGREES_FOR_ONE_MINUTE;
+ mSelectionDegrees[MINUTES] = (minute % MINUTES_IN_CIRCLE) * DEGREES_FOR_ONE_MINUTE;
invalidate();
@@ -1140,8 +1140,8 @@
// If the touched minute is closer to the current minute
// than it is to the snapped minute, return current.
- final int currentOffset = getCircularDiff(current, touched, MINUTES_IN_HOUR);
- final int snappedOffset = getCircularDiff(snapped, touched, MINUTES_IN_HOUR);
+ final int currentOffset = getCircularDiff(current, touched, MINUTES_IN_CIRCLE);
+ final int snappedOffset = getCircularDiff(snapped, touched, MINUTES_IN_CIRCLE);
final int minute;
if (currentOffset < snappedOffset) {
minute = current;
@@ -1181,7 +1181,7 @@
}
} else {
final int current = getCurrentMinute();
- for (int i = 0; i < MINUTES_IN_HOUR; i += MINUTE_INCREMENT) {
+ for (int i = 0; i < MINUTES_IN_CIRCLE; i += MINUTE_INCREMENT) {
virtualViewIds.add(makeId(TYPE_MINUTE, i));
// If the current minute falls between two increments,
@@ -1239,7 +1239,7 @@
if (value < current && nextValue > current) {
// The current value is between two snap values.
return makeId(type, current);
- } else if (nextValue < MINUTES_IN_HOUR) {
+ } else if (nextValue < MINUTES_IN_CIRCLE) {
return makeId(type, nextValue);
}
}
diff --git a/docs/html/about/dashboards/index.jd b/docs/html/about/dashboards/index.jd
index cfb65a5..52f086e 100644
--- a/docs/html/about/dashboards/index.jd
+++ b/docs/html/about/dashboards/index.jd
@@ -57,7 +57,7 @@
</div>
-<p style="clear:both"><em>Data collected during a 7-day period ending on March 2, 2015.
+<p style="clear:both"><em>Data collected during a 7-day period ending on April 6, 2015.
<br/>Any versions with less than 0.1% distribution are not shown.</em>
</p>
@@ -88,7 +88,7 @@
</div>
-<p style="clear:both"><em>Data collected during a 7-day period ending on March 2, 2015.
+<p style="clear:both"><em>Data collected during a 7-day period ending on April 6, 2015.
<br/>Any screen configurations with less than 0.1% distribution are not shown.</em></p>
@@ -108,8 +108,7 @@
<img alt="" style="float:right"
-src="//chart.googleapis.com/chart?chl=GL%202.0%7CGL%203.0&chf=bg%2Cs%2C00000000&chd=t%3A67.5%2C32.5&chco=c4df9b%2C6fad0c&cht=p&chs=400x250" />
-
+src="//chart.googleapis.com/chart?chl=GL%202.0%7CGL%203.0%7CGL%203.1&chf=bg%2Cs%2C00000000&chd=t%3A65.9%2C33.8%2C0.3&chco=c4df9b%2C6fad0c&cht=p&chs=400x250">
<p>To declare which version of OpenGL ES your application requires, you should use the {@code
android:glEsVersion} attribute of the <a
@@ -127,17 +126,21 @@
</tr>
<tr>
<td>2.0</td>
-<td>67.5%</td>
+<td>65.9%</td>
</tr>
<tr>
<td>3.0</td>
-<td>32.5%</td>
+<td>33.8%</td>
+</tr>
+<tr>
+<td>3.1</td>
+<td>0.3%</td>
</tr>
</table>
-<p style="clear:both"><em>Data collected during a 7-day period ending on March 2, 2015</em></p>
+<p style="clear:both"><em>Data collected during a 7-day period ending on April 6, 2015</em></p>
@@ -155,7 +158,7 @@
var VERSION_DATA =
[
{
- "chart": "//chart.googleapis.com/chart?chl=Froyo%7CGingerbread%7CIce%20Cream%20Sandwich%7CJelly%20Bean%7CKitKat%7CLollipop&chco=c4df9b%2C6fad0c&chd=t%3A0.4%2C6.9%2C5.9%2C42.6%2C40.9%2C3.3&chf=bg%2Cs%2C00000000&chs=500x250&cht=p",
+ "chart": "//chart.googleapis.com/chart?chl=Froyo%7CGingerbread%7CIce%20Cream%20Sandwich%7CJelly%20Bean%7CKitKat%7CLollipop&chf=bg%2Cs%2C00000000&chd=t%3A0.4%2C6.4%2C5.7%2C40.7%2C41.4%2C5.4&chco=c4df9b%2C6fad0c&chs=500x250&cht=p",
"data": [
{
"api": 8,
@@ -165,37 +168,42 @@
{
"api": 10,
"name": "Gingerbread",
- "perc": "6.9"
+ "perc": "6.4"
},
{
"api": 15,
"name": "Ice Cream Sandwich",
- "perc": "5.9"
+ "perc": "5.7"
},
{
"api": 16,
"name": "Jelly Bean",
- "perc": "17.3"
+ "perc": "16.5"
},
{
"api": 17,
"name": "Jelly Bean",
- "perc": "19.4"
+ "perc": "18.6"
},
{
"api": 18,
"name": "Jelly Bean",
- "perc": "5.9"
+ "perc": "5.6"
},
{
"api": 19,
"name": "KitKat",
- "perc": "40.9"
+ "perc": "41.4"
},
{
"api": 21,
"name": "Lollipop",
- "perc": "3.3"
+ "perc": "5.0"
+ },
+ {
+ "api": 22,
+ "name": "Lollipop",
+ "perc": "0.4"
}
]
}
@@ -208,29 +216,29 @@
"data": {
"Large": {
"hdpi": "0.6",
- "ldpi": "0.5",
- "mdpi": "5.1",
- "tvdpi": "2.3",
+ "ldpi": "0.4",
+ "mdpi": "4.8",
+ "tvdpi": "2.2",
"xhdpi": "0.6"
},
"Normal": {
- "hdpi": "38.7",
- "mdpi": "8.4",
+ "hdpi": "39.3",
+ "mdpi": "8.1",
"tvdpi": "0.1",
- "xhdpi": "18.9",
- "xxhdpi": "15.8"
+ "xhdpi": "19.5",
+ "xxhdpi": "15.9"
},
"Small": {
- "ldpi": "4.6"
+ "ldpi": "4.4"
},
"Xlarge": {
"hdpi": "0.3",
- "mdpi": "3.5",
+ "mdpi": "3.2",
"xhdpi": "0.6"
}
},
- "densitychart": "//chart.googleapis.com/chart?chl=ldpi%7Cmdpi%7Ctvdpi%7Chdpi%7Cxhdpi%7Cxxhdpi&chco=c4df9b%2C6fad0c&chd=t%3A5.1%2C17.0%2C2.4%2C39.6%2C20.1%2C15.8&chf=bg%2Cs%2C00000000&chs=400x250&cht=p",
- "layoutchart": "//chart.googleapis.com/chart?chl=Xlarge%7CLarge%7CNormal%7CSmall&chco=c4df9b%2C6fad0c&chd=t%3A4.4%2C9.1%2C81.9%2C4.6&chf=bg%2Cs%2C00000000&chs=400x250&cht=p"
+ "densitychart": "//chart.googleapis.com/chart?chl=ldpi%7Cmdpi%7Ctvdpi%7Chdpi%7Cxhdpi%7Cxxhdpi&chf=bg%2Cs%2C00000000&chd=t%3A4.8%2C16.1%2C2.3%2C40.2%2C20.7%2C15.9&chco=c4df9b%2C6fad0c&chs=400x250&cht=p",
+ "layoutchart": "//chart.googleapis.com/chart?chl=Xlarge%7CLarge%7CNormal%7CSmall&chf=bg%2Cs%2C00000000&chd=t%3A4.1%2C8.6%2C82.9%2C4.4&chco=c4df9b%2C6fad0c&chs=400x250&cht=p"
}
];
@@ -312,6 +320,11 @@
"api":21,
"link":"<a href='/about/versions/android-5.0.html'>5.0</a>",
"codename":"Lollipop"
+ },
+ {
+ "api":22,
+ "link":"<a href='/about/versions/android-5.1.html'>5.1</a>",
+ "codename":"Lollipop"
}
];
diff --git a/docs/html/google/play-services/setup.jd b/docs/html/google/play-services/setup.jd
index 3f71d04..e75235e 100644
--- a/docs/html/google/play-services/setup.jd
+++ b/docs/html/google/play-services/setup.jd
@@ -82,14 +82,6 @@
<img src="{@docRoot}images/tools/sync-project.png" style="vertical-align:bottom;margin:0;height:19px" />
in the toolbar.
</li>
- <li>Open your app's manifest file and add the following tag as a child of the <a
-href="{@docRoot}guide/topics/manifest/application-element.html">{@code <application>}</a>
-element:
-<pre>
-<meta-data android:name="com.google.android.gms.version"
- android:value="@integer/google_play_services_version" />
-</pre>
- </li>
</ol>
<p>You can now begin developing features with the
diff --git a/docs/html/google/play/billing/billing_integrate.jd b/docs/html/google/play/billing/billing_integrate.jd
index e3cacf9..eb58af4 100644
--- a/docs/html/google/play/billing/billing_integrate.jd
+++ b/docs/html/google/play/billing/billing_integrate.jd
@@ -34,7 +34,7 @@
<h2>See also</h2>
<ol>
<li><a href="{@docRoot}training/in-app-billing/index.html">Selling In-app Products</a></li>
- </ol>
+ </ol>
</div>
</div>
@@ -42,26 +42,26 @@
<p class="note"><strong>Note:</strong> To see a complete implementation and learn how to test your application, see the <a href="{@docRoot}training/in-app-billing/index.html">Selling In-app Products</a> training class. The training class provides a complete sample In-app Billing application, including convenience classes to handle key tasks related to setting up your connection, sending billing requests and processing responses from Google Play, and managing background threading so that you can make In-app Billing calls from your main activity.</p>
-<p>Before you start, be sure that you read the <a href="{@docRoot}google/play/billing/billing_overview.html">In-app Billing Overview</a> to familiarize yourself with
+<p>Before you start, be sure that you read the <a href="{@docRoot}google/play/billing/billing_overview.html">In-app Billing Overview</a> to familiarize yourself with
concepts that will make it easier for you to implement In-app Billing.</p>
-<p>To implement In-app Billing in your application, you need to do the
+<p>To implement In-app Billing in your application, you need to do the
following:</p>
<ol>
<li>Add the In-app Billing library to your project.</li>
<li>Update your {@code AndroidManifest.xml} file.</li>
- <li>Create a {@code ServiceConnection} and bind it to
+ <li>Create a {@code ServiceConnection} and bind it to
{@code IInAppBillingService}.</li>
- <li>Send In-app Billing requests from your application to
+ <li>Send In-app Billing requests from your application to
{@code IInAppBillingService}.</li>
<li>Handle In-app Billing responses from Google Play.</li>
</ol>
<h2 id="billing-add-aidl">Adding the AIDL file to your project</h2>
-<p>{@code IInAppBillingService.aidl} is an Android Interface Definition
-Language (AIDL) file that defines the interface to the In-app Billing Version
-3 service. You will use this interface to make billing requests by invoking IPC
+<p>{@code IInAppBillingService.aidl} is an Android Interface Definition
+Language (AIDL) file that defines the interface to the In-app Billing Version
+3 service. You will use this interface to make billing requests by invoking IPC
method calls.</p>
<p>To get the AIDL file:</p>
<ol>
@@ -76,28 +76,28 @@
<ol>
<li>Copy the {@code IInAppBillingService.aidl} file to your Android project.
<ul>
- <li>If you are using Eclipse:
+ <li>If you are using Eclipse:
<ol type="a">
- <li>If you are starting from an existing Android project, open the project
-in Eclipse. If you are creating a new Android project from scratch, click
-<strong>File</strong> > <strong>New</strong> > <strong>Android Application
-Project</strong>, then follow the instructions in the <strong>New Android
+ <li>If you are starting from an existing Android project, open the project
+in Eclipse. If you are creating a new Android project from scratch, click
+<strong>File</strong> > <strong>New</strong> > <strong>Android Application
+Project</strong>, then follow the instructions in the <strong>New Android
Application</strong> wizard to create a new project in your workspace.</li>
- <li>In the {@code /src} directory, click <strong>File</strong> >
+ <li>In the {@code /src} directory, click <strong>File</strong> >
<strong>New</strong> > <strong>Package</strong>, then create a package named {@code com.android.vending.billing}.</li>
- <li>Copy the {@code IInAppBillingService.aidl} file from {@code <sdk>/extras/google/play_billing/} and paste it into the {@code src/com.android.vending.billing/}
+ <li>Copy the {@code IInAppBillingService.aidl} file from {@code <sdk>/extras/google/play_billing/} and paste it into the {@code src/com.android.vending.billing/}
folder in your workspace.</li>
</ol>
</li>
- <li>If you are developing in a non-Eclipse environment: Create the following
-directory {@code /src/com/android/vending/billing} and copy the
-{@code IInAppBillingService.aidl} file into this directory. Put the AIDL file
+ <li>If you are developing in a non-Eclipse environment: Create the following
+directory {@code /src/com/android/vending/billing} and copy the
+{@code IInAppBillingService.aidl} file into this directory. Put the AIDL file
into your project and use the Ant tool to build your project so that the
<code>IInAppBillingService.java</code> file gets generated.</li>
</ul>
</li>
-<li>Build your application. You should see a generated file named
-{@code IInAppBillingService.java} in the {@code /gen} directory of your
+<li>Build your application. You should see a generated file named
+{@code IInAppBillingService.java} in the {@code /gen} directory of your
project.</li>
</ol>
@@ -135,7 +135,7 @@
}
@Override
- public void onServiceConnected(ComponentName name,
+ public void onServiceConnected(ComponentName name,
IBinder service) {
mService = IInAppBillingService.Stub.asInterface(service);
}
@@ -162,7 +162,7 @@
super.onDestroy();
if (mService != null) {
unbindService(mServiceConn);
- }
+ }
}
</pre>
@@ -185,13 +185,13 @@
</pre>
<p>To retrieve this information from Google Play, call the {@code getSkuDetails} method on the In-app Billing Version 3 API, and pass the method the In-app Billing API version (“3”), the package name of your calling app, the purchase type (“inapp”), and the {@link android.os.Bundle} that you created.</p>
<pre>
-Bundle skuDetails = mService.getSkuDetails(3,
+Bundle skuDetails = mService.getSkuDetails(3,
getPackageName(), "inapp", querySkus);
</pre>
<p>If the request is successful, the returned {@link android.os.Bundle}has a response code of {@code BILLING_RESPONSE_RESULT_OK} (0).</p>
<p class="note"><strong>Warning:</strong> Do not call the {@code getSkuDetails} method on the main thread. Calling this method triggers a network request which could block your main thread. Instead, create a separate thread and call the {@code getSkuDetails} method from inside that thread.</p>
-<p>To see all the possible response codes from Google Play, see <a href="{@docRoot}google/play/billing/billing_reference.html#billing-codes">In-app Billing Reference</a>.</p>
+<p>To see all the possible response codes from Google Play, see <a href="{@docRoot}google/play/billing/billing_reference.html#billing-codes">In-app Billing Reference</a>.</p>
<p>The query results are stored in a String ArrayList with key {@code DETAILS_LIST}. The purchase information is stored in the String in JSON format. To see the types of product detail information that are returned, see <a href="{@docRoot}google/play/billing/billing_reference.html#getSkuDetails">In-app Billing Reference</a>.</p>
@@ -201,7 +201,7 @@
if (response == 0) {
ArrayList<String> responseList
= skuDetails.getStringArrayList("DETAILS_LIST");
-
+
for (String thisResponse : responseList) {
JSONObject object = new JSONObject(thisResponse);
String sku = object.getString("productId");
@@ -232,12 +232,12 @@
1001, new Intent(), Integer.valueOf(0), Integer.valueOf(0),
Integer.valueOf(0));
</pre>
-<p>Google Play sends a response to your {@link android.app.PendingIntent} to the {@link android.app.Activity#onActivityResult onActivityResult} method of your application. The {@link android.app.Activity#onActivityResult onActivityResult} method will have a result code of {@code Activity.RESULT_OK} (1) or {@code Activity.RESULT_CANCELED} (0). To see the types of order information that is returned in the response {@link android.content.Intent}, see <a href="{@docRoot}google/play/billing/billing_reference.html#getBuyIntent">In-app Billing Reference</a>.</p>
+<p>Google Play sends a response to your {@link android.app.PendingIntent} to the {@link android.app.Activity#onActivityResult onActivityResult} method of your application. The {@link android.app.Activity#onActivityResult onActivityResult} method will have a result code of {@code Activity.RESULT_OK} (1) or {@code Activity.RESULT_CANCELED} (0). To see the types of order information that is returned in the response {@link android.content.Intent}, see <a href="{@docRoot}google/play/billing/billing_reference.html#getBuyIntent">In-app Billing Reference</a>.</p>
<p>The purchase data for the order is a String in JSON format that is mapped to the {@code INAPP_PURCHASE_DATA} key in the response {@link android.content.Intent}, for example:
<pre>
-'{
- "orderId":"12999763169054705758.1371079406387615",
+'{
+ "orderId":"12999763169054705758.1371079406387615",
"packageName":"com.example.app",
"productId":"exampleSku",
"purchaseTime":1345678900000,
@@ -259,17 +259,17 @@
<p>Continuing from the previous example, you get the response code, purchase data, and signature from the response {@link android.content.Intent}.</p>
<pre>
@Override
-protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == 1001) {
+protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == 1001) {
int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");
-
+
if (resultCode == RESULT_OK) {
try {
JSONObject jo = new JSONObject(purchaseData);
String sku = jo.getString("productId");
- alert("You have bought the " + sku + ". Excellent choice,
+ alert("You have bought the " + sku + ". Excellent choice,
adventurer!");
}
catch (JSONException e) {
@@ -298,45 +298,45 @@
ArrayList<String> purchaseDataList =
ownedItems.getStringArrayList("INAPP_PURCHASE_DATA_LIST");
ArrayList<String> signatureList =
- ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE");
- String continuationToken =
+ ownedItems.getStringArrayList("INAPP_DATA_SIGNATURE_LIST");
+ String continuationToken =
ownedItems.getString("INAPP_CONTINUATION_TOKEN");
-
+
for (int i = 0; i < purchaseDataList.size(); ++i) {
String purchaseData = purchaseDataList.get(i);
String signature = signatureList.get(i);
String sku = ownedSkus.get(i);
-
+
// do something with this purchase information
// e.g. display the updated list of products owned by user
- }
+ }
- // if continuationToken != null, call getPurchases again
+ // if continuationToken != null, call getPurchases again
// and pass in the token to retrieve more items
}
</pre>
<h3 id="Consume">Consuming a Purchase</h3>
-<p>You can use the In-app Billing Version 3 API to track the ownership of
-purchased in-app products in Google Play. Once an in-app product is purchased,
-it is considered to be "owned" and cannot be purchased from Google Play. You
-must send a consumption request for the in-app product before Google Play makes
+<p>You can use the In-app Billing Version 3 API to track the ownership of
+purchased in-app products in Google Play. Once an in-app product is purchased,
+it is considered to be "owned" and cannot be purchased from Google Play. You
+must send a consumption request for the in-app product before Google Play makes
it available for purchase again.</p>
-<p class="caution"><strong>Important</strong>: Managed in-app products are
+<p class="caution"><strong>Important</strong>: Managed in-app products are
consumable, but subscriptions are not.</p>
-<p>How you use the consumption mechanism in your app is up to you. Typically,
-you would implement consumption for in-app products with temporary benefits that
-users may want to purchase multiple times (for example, in-game currency or
-equipment). You would typically not want to implement consumption for in-app
-products that are purchased once and provide a permanent effect (for example,
+<p>How you use the consumption mechanism in your app is up to you. Typically,
+you would implement consumption for in-app products with temporary benefits that
+users may want to purchase multiple times (for example, in-game currency or
+equipment). You would typically not want to implement consumption for in-app
+products that are purchased once and provide a permanent effect (for example,
a premium upgrade).</p>
-<p>To record a purchase consumption, send the {@code consumePurchase} method to
-the In-app Billing service and pass in the {@code purchaseToken} String value
-that identifies the purchase to be removed. The {@code purchaseToken} is part
-of the data returned in the {@code INAPP_PURCHASE_DATA} String by the Google
-Play service following a successful purchase request. In this example, you are
-recording the consumption of a product that is identified with the
+<p>To record a purchase consumption, send the {@code consumePurchase} method to
+the In-app Billing service and pass in the {@code purchaseToken} String value
+that identifies the purchase to be removed. The {@code purchaseToken} is part
+of the data returned in the {@code INAPP_PURCHASE_DATA} String by the Google
+Play service following a successful purchase request. In this example, you are
+recording the consumption of a product that is identified with the
{@code purchaseToken} in the {@code token} variable.</p>
<pre>
int response = mService.consumePurchase(3, getPackageName(), token);
@@ -346,10 +346,10 @@
<p class="note"><strong>Security Recommendation:</strong> You must send a consumption request before provisioning the benefit of the consumable in-app purchase to the user. Make sure that you have received a successful consumption response from Google Play before you provision the item.</p>
<h3 id="Subs">Implementing Subscriptions</h3>
-<p>Launching a purchase flow for a subscription is similar to launching the
-purchase flow for a product, with the exception that the product type must be set
-to "subs". The purchase result is delivered to your Activity's
-{@link android.app.Activity#onActivityResult onActivityResult} method, exactly
+<p>Launching a purchase flow for a subscription is similar to launching the
+purchase flow for a product, with the exception that the product type must be set
+to "subs". The purchase result is delivered to your Activity's
+{@link android.app.Activity#onActivityResult onActivityResult} method, exactly
as in the case of in-app products.</p>
<pre>
Bundle bundle = mService.getBuyIntent(3, "com.example.myapp",
@@ -363,39 +363,39 @@
Integer.valueOf(0), Integer.valueOf(0), Integer.valueOf(0));
}
</pre>
-<p>To query for active subscriptions, use the {@code getPurchases} method, again
+<p>To query for active subscriptions, use the {@code getPurchases} method, again
with the product type parameter set to "subs".</p>
<pre>
Bundle activeSubs = mService.getPurchases(3, "com.example.myapp",
"subs", continueToken);
</pre>
-<p>The call returns a {@code Bundle} with all the active subscriptions owned by
-the user. Once a subscription expires without renewal, it will no longer appear
+<p>The call returns a {@code Bundle} with all the active subscriptions owned by
+the user. Once a subscription expires without renewal, it will no longer appear
in the returned {@code Bundle}.</p>
<h2 id="billing-security">Securing Your Application</h2>
-<p>To help ensure the integrity of the transaction information that is sent to
-your application, Google Play signs the JSON string that contains the response
-data for a purchase order. Google Play uses the private key that is associated
-with your application in the Developer Console to create this signature. The
+<p>To help ensure the integrity of the transaction information that is sent to
+your application, Google Play signs the JSON string that contains the response
+data for a purchase order. Google Play uses the private key that is associated
+with your application in the Developer Console to create this signature. The
Developer Console generates an RSA key pair for each application.<p>
-<p class="note"><strong>Note:</strong>To find the public key portion of this key
-pair, open your application's details in the Developer Console, then click on
-<strong>Services & APIs</strong>, and look at the field titled
+<p class="note"><strong>Note:</strong>To find the public key portion of this key
+pair, open your application's details in the Developer Console, then click on
+<strong>Services & APIs</strong>, and look at the field titled
<strong>Your License Key for This Application</strong>.</p>
-<p>The Base64-encoded RSA public key generated by Google Play is in binary
-encoded, X.509 subjectPublicKeyInfo DER SEQUENCE format. It is the same public
+<p>The Base64-encoded RSA public key generated by Google Play is in binary
+encoded, X.509 subjectPublicKeyInfo DER SEQUENCE format. It is the same public
key that is used with Google Play licensing.</p>
-<p>When your application receives this signed response you can
-use the public key portion of your RSA key pair to verify the signature.
-By performing signature verification you can detect responses that have
-been tampered with or that have been spoofed. You can perform this signature
-verification step in your application; however, if your application connects
-to a secure remote server then we recommend that you perform the signature
+<p>When your application receives this signed response you can
+use the public key portion of your RSA key pair to verify the signature.
+By performing signature verification you can detect responses that have
+been tampered with or that have been spoofed. You can perform this signature
+verification step in your application; however, if your application connects
+to a secure remote server then we recommend that you perform the signature
verification on that server.</p>
<p>For more information about best practices for security and design, see <a
diff --git a/libs/hwui/JankTracker.cpp b/libs/hwui/JankTracker.cpp
index 65be9e1..a72faea 100644
--- a/libs/hwui/JankTracker.cpp
+++ b/libs/hwui/JankTracker.cpp
@@ -169,6 +169,10 @@
newData->jankFrameCount += mData->jankFrameCount;
newData->totalFrameCount >>= divider;
newData->totalFrameCount += mData->totalFrameCount;
+ if (newData->statStartTime > mData->statStartTime
+ || newData->statStartTime == 0) {
+ newData->statStartTime = mData->statStartTime;
+ }
freeData();
mData = newData;
@@ -235,6 +239,7 @@
}
void JankTracker::dumpData(const ProfileData* data, int fd) {
+ dprintf(fd, "\nStats since: %lluns", data->statStartTime);
dprintf(fd, "\nTotal frames rendered: %u", data->totalFrameCount);
dprintf(fd, "\nJanky frames: %u (%.2f%%)", data->jankFrameCount,
(float) data->jankFrameCount / (float) data->totalFrameCount * 100.0f);
@@ -252,6 +257,7 @@
mData->frameCounts.fill(0);
mData->totalFrameCount = 0;
mData->jankFrameCount = 0;
+ mData->statStartTime = systemTime(CLOCK_MONOTONIC);
}
uint32_t JankTracker::findPercentile(const ProfileData* data, int percentile) {
diff --git a/libs/hwui/JankTracker.h b/libs/hwui/JankTracker.h
index 4783001..3887e5e 100644
--- a/libs/hwui/JankTracker.h
+++ b/libs/hwui/JankTracker.h
@@ -44,10 +44,11 @@
struct ProfileData {
std::array<uint32_t, NUM_BUCKETS> jankTypeCounts;
// See comments on kBucket* constants for what this holds
- std::array<uint32_t, 57> frameCounts;
+ std::array<uint32_t, 55> frameCounts;
uint32_t totalFrameCount;
uint32_t jankFrameCount;
+ nsecs_t statStartTime;
};
// TODO: Replace DrawProfiler with this
diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java
index 98bfaff..4c5fb40 100644
--- a/media/java/android/media/AudioTrack.java
+++ b/media/java/android/media/AudioTrack.java
@@ -1499,7 +1499,9 @@
* @param sizeInShorts the number of shorts to read in audioData after the offset.
* @return the number of shorts that were written or {@link #ERROR_INVALID_OPERATION}
* if the object wasn't properly initialized, or {@link #ERROR_BAD_VALUE} if
- * the parameters don't resolve to valid data and indexes.
+ * the parameters don't resolve to valid data and indexes, or
+ * {@link AudioManager#ERROR_DEAD_OBJECT} if the AudioTrack is not valid anymore and
+ * needs to be recreated.
*/
public int write(short[] audioData, int offsetInShorts, int sizeInShorts) {
@@ -1559,7 +1561,9 @@
* queuing as much audio data for playback as possible without blocking.
* @return the number of floats that were written, or {@link #ERROR_INVALID_OPERATION}
* if the object wasn't properly initialized, or {@link #ERROR_BAD_VALUE} if
- * the parameters don't resolve to valid data and indexes.
+ * the parameters don't resolve to valid data and indexes, or
+ * {@link AudioManager#ERROR_DEAD_OBJECT} if the AudioTrack is not valid anymore and
+ * needs to be recreated.
*/
public int write(float[] audioData, int offsetInFloats, int sizeInFloats,
@WriteMode int writeMode) {
@@ -1620,7 +1624,9 @@
* <BR>With {@link #WRITE_NON_BLOCKING}, the write will return immediately after
* queuing as much audio data for playback as possible without blocking.
* @return 0 or a positive number of bytes that were written, or
- * {@link #ERROR_BAD_VALUE}, {@link #ERROR_INVALID_OPERATION}
+ * {@link #ERROR_BAD_VALUE}, {@link #ERROR_INVALID_OPERATION}, or
+ * {@link AudioManager#ERROR_DEAD_OBJECT} if the AudioTrack is not valid anymore and
+ * needs to be recreated.
*/
public int write(ByteBuffer audioData, int sizeInBytes,
@WriteMode int writeMode) {
diff --git a/packages/StatementService/Android.mk b/packages/StatementService/Android.mk
new file mode 100644
index 0000000..f0adb1c
--- /dev/null
+++ b/packages/StatementService/Android.mk
@@ -0,0 +1,33 @@
+# Copyright (C) 2015 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_PACKAGE_NAME := StatementService
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ libprotobuf-java-nano \
+ volley
+
+include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH)/src)
diff --git a/packages/StatementService/AndroidManifest.xml b/packages/StatementService/AndroidManifest.xml
new file mode 100644
index 0000000..3ee453b
--- /dev/null
+++ b/packages/StatementService/AndroidManifest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.statementservice"
+ android:versionCode="1"
+ android:versionName="1.0">
+
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.INTENT_FILTER_VERIFICATION_AGENT"/>
+
+ <application
+ android:label="@string/service_name"
+ android:allowBackup="false">
+ <service
+ android:name=".DirectStatementService"
+ android:exported="false">
+ <intent-filter>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <action android:name="com.android.statementservice.aosp.service.CHECK_ACTION"/>
+ </intent-filter>
+ </service>
+
+ <receiver
+ android:name=".IntentFilterVerificationReceiver"
+ android:permission="android.permission.BIND_INTENT_FILTER_VERIFIER">
+ <!-- Set the priority 1 so newer implementation can have higher priority. -->
+ <intent-filter
+ android:priority="1">
+ <action android:name="android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION"/>
+ <data android:mimeType="application/vnd.android.package-archive"/>
+ </intent-filter>
+ </receiver>
+
+ </application>
+
+</manifest>
diff --git a/packages/StatementService/proguard.flags b/packages/StatementService/proguard.flags
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/StatementService/proguard.flags
diff --git a/packages/StatementService/res/values/strings.xml b/packages/StatementService/res/values/strings.xml
new file mode 100644
index 0000000..df6d80b
--- /dev/null
+++ b/packages/StatementService/res/values/strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="service_name">Intent Filter Verification Service</string>
+</resources>
diff --git a/packages/StatementService/src/com/android/statementservice/DirectStatementService.java b/packages/StatementService/src/com/android/statementservice/DirectStatementService.java
new file mode 100644
index 0000000..449738e
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/DirectStatementService.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2015 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.statementservice;
+
+import android.app.Service;
+import android.content.Intent;
+import android.net.http.HttpResponseCache;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.android.statementservice.retriever.AbstractAsset;
+import com.android.statementservice.retriever.AbstractAssetMatcher;
+import com.android.statementservice.retriever.AbstractStatementRetriever;
+import com.android.statementservice.retriever.AbstractStatementRetriever.Result;
+import com.android.statementservice.retriever.AssociationServiceException;
+import com.android.statementservice.retriever.Relation;
+import com.android.statementservice.retriever.Statement;
+
+import org.json.JSONException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/**
+ * Handles com.android.statementservice.service.CHECK_ALL_ACTION intents.
+ */
+public final class DirectStatementService extends Service {
+ private static final String TAG = DirectStatementService.class.getSimpleName();
+
+ /**
+ * Returns true if every asset in {@code SOURCE_ASSET_DESCRIPTORS} is associated with {@code
+ * EXTRA_TARGET_ASSET_DESCRIPTOR} for {@code EXTRA_RELATION} relation.
+ *
+ * <p>Takes parameter {@code EXTRA_RELATION}, {@code SOURCE_ASSET_DESCRIPTORS}, {@code
+ * EXTRA_TARGET_ASSET_DESCRIPTOR}, and {@code EXTRA_RESULT_RECEIVER}.
+ */
+ public static final String CHECK_ALL_ACTION =
+ "com.android.statementservice.service.CHECK_ALL_ACTION";
+
+ /**
+ * Parameter for {@link #CHECK_ALL_ACTION}.
+ *
+ * <p>A relation string.
+ */
+ public static final String EXTRA_RELATION =
+ "com.android.statementservice.service.RELATION";
+
+ /**
+ * Parameter for {@link #CHECK_ALL_ACTION}.
+ *
+ * <p>An array of asset descriptors in JSON.
+ */
+ public static final String EXTRA_SOURCE_ASSET_DESCRIPTORS =
+ "com.android.statementservice.service.SOURCE_ASSET_DESCRIPTORS";
+
+ /**
+ * Parameter for {@link #CHECK_ALL_ACTION}.
+ *
+ * <p>An asset descriptor in JSON.
+ */
+ public static final String EXTRA_TARGET_ASSET_DESCRIPTOR =
+ "com.android.statementservice.service.TARGET_ASSET_DESCRIPTOR";
+
+ /**
+ * Parameter for {@link #CHECK_ALL_ACTION}.
+ *
+ * <p>A {@code ResultReceiver} instance that will be used to return the result. If the request
+ * failed, return {@link #RESULT_FAIL} and an empty {@link android.os.Bundle}. Otherwise, return
+ * {@link #RESULT_SUCCESS} and a {@link android.os.Bundle} with the result stored in {@link
+ * #IS_ASSOCIATED}.
+ */
+ public static final String EXTRA_RESULT_RECEIVER =
+ "com.android.statementservice.service.RESULT_RECEIVER";
+
+ /**
+ * A boolean bundle entry that stores the result of {@link #CHECK_ALL_ACTION}.
+ * This is set only if the service returns with {@code RESULT_SUCCESS}.
+ * {@code IS_ASSOCIATED} is true if and only if {@code FAILED_SOURCES} is empty.
+ */
+ public static final String IS_ASSOCIATED = "is_associated";
+
+ /**
+ * A String ArrayList bundle entry that stores sources that can't be verified.
+ */
+ public static final String FAILED_SOURCES = "failed_sources";
+
+ /**
+ * Returned by the service if the request is successfully processed. The caller should check
+ * the {@code IS_ASSOCIATED} field to determine if the association exists or not.
+ */
+ public static final int RESULT_SUCCESS = 0;
+
+ /**
+ * Returned by the service if the request failed. The request will fail if, for example, the
+ * input is not well formed, or the network is not available.
+ */
+ public static final int RESULT_FAIL = 1;
+
+ private static final long HTTP_CACHE_SIZE_IN_BYTES = 1 * 1024 * 1024; // 1 MBytes
+ private static final String CACHE_FILENAME = "request_cache";
+
+ private AbstractStatementRetriever mStatementRetriever;
+ private Handler mHandler;
+ private HandlerThread mThread;
+ private HttpResponseCache mHttpResponseCache;
+
+ @Override
+ public void onCreate() {
+ mThread = new HandlerThread("DirectStatementService thread",
+ android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ mThread.start();
+ onCreate(AbstractStatementRetriever.createDirectRetriever(this), mThread.getLooper(),
+ getCacheDir());
+ }
+
+ /**
+ * Creates a DirectStatementService with the dependencies passed in for easy testing.
+ */
+ public void onCreate(AbstractStatementRetriever statementRetriever, Looper looper,
+ File cacheDir) {
+ super.onCreate();
+ mStatementRetriever = statementRetriever;
+ mHandler = new Handler(looper);
+
+ try {
+ File httpCacheDir = new File(cacheDir, CACHE_FILENAME);
+ mHttpResponseCache = HttpResponseCache.install(httpCacheDir, HTTP_CACHE_SIZE_IN_BYTES);
+ } catch (IOException e) {
+ Log.i(TAG, "HTTPS response cache installation failed:" + e);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mThread != null) {
+ mThread.quit();
+ }
+
+ try {
+ if (mHttpResponseCache != null) {
+ mHttpResponseCache.delete();
+ }
+ } catch (IOException e) {
+ Log.i(TAG, "HTTP(S) response cache deletion failed:" + e);
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ super.onStartCommand(intent, flags, startId);
+
+ if (intent == null) {
+ Log.e(TAG, "onStartCommand called with null intent");
+ return START_STICKY;
+ }
+
+ if (intent.getAction().equals(CHECK_ALL_ACTION)) {
+
+ Bundle extras = intent.getExtras();
+ List<String> sources = extras.getStringArrayList(EXTRA_SOURCE_ASSET_DESCRIPTORS);
+ String target = extras.getString(EXTRA_TARGET_ASSET_DESCRIPTOR);
+ String relation = extras.getString(EXTRA_RELATION);
+ ResultReceiver resultReceiver = extras.getParcelable(EXTRA_RESULT_RECEIVER);
+
+ if (resultReceiver == null) {
+ Log.e(TAG, " Intent does not have extra " + EXTRA_RESULT_RECEIVER);
+ return START_STICKY;
+ }
+ if (sources == null) {
+ Log.e(TAG, " Intent does not have extra " + EXTRA_SOURCE_ASSET_DESCRIPTORS);
+ resultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+ return START_STICKY;
+ }
+ if (target == null) {
+ Log.e(TAG, " Intent does not have extra " + EXTRA_TARGET_ASSET_DESCRIPTOR);
+ resultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+ return START_STICKY;
+ }
+ if (relation == null) {
+ Log.e(TAG, " Intent does not have extra " + EXTRA_RELATION);
+ resultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+ return START_STICKY;
+ }
+
+ mHandler.post(new ExceptionLoggingFutureTask<Void>(
+ new IsAssociatedCallable(sources, target, relation, resultReceiver), TAG));
+ } else {
+ Log.e(TAG, "onStartCommand called with unsupported action: " + intent.getAction());
+ }
+ return START_STICKY;
+ }
+
+ private class IsAssociatedCallable implements Callable<Void> {
+
+ private List<String> mSources;
+ private String mTarget;
+ private String mRelation;
+ private ResultReceiver mResultReceiver;
+
+ public IsAssociatedCallable(List<String> sources, String target, String relation,
+ ResultReceiver resultReceiver) {
+ mSources = sources;
+ mTarget = target;
+ mRelation = relation;
+ mResultReceiver = resultReceiver;
+ }
+
+ private boolean verifyOneSource(AbstractAsset source, AbstractAssetMatcher target,
+ Relation relation) throws AssociationServiceException {
+ Result statements = mStatementRetriever.retrieveStatements(source);
+ for (Statement statement : statements.getStatements()) {
+ if (relation.matches(statement.getRelation())
+ && target.matches(statement.getTarget())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Void call() {
+ Bundle result = new Bundle();
+ ArrayList<String> failedSources = new ArrayList<String>();
+ AbstractAssetMatcher target;
+ Relation relation;
+ try {
+ target = AbstractAssetMatcher.createMatcher(mTarget);
+ relation = Relation.create(mRelation);
+ } catch (AssociationServiceException | JSONException e) {
+ Log.e(TAG, "isAssociatedCallable failed with exception", e);
+ mResultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+ return null;
+ }
+
+ boolean allSourcesVerified = true;
+ for (String sourceString : mSources) {
+ AbstractAsset source;
+ try {
+ source = AbstractAsset.create(sourceString);
+ } catch (AssociationServiceException e) {
+ mResultReceiver.send(RESULT_FAIL, Bundle.EMPTY);
+ return null;
+ }
+
+ try {
+ if (!verifyOneSource(source, target, relation)) {
+ failedSources.add(source.toJson());
+ allSourcesVerified = false;
+ }
+ } catch (AssociationServiceException e) {
+ failedSources.add(source.toJson());
+ allSourcesVerified = false;
+ }
+ }
+
+ result.putBoolean(IS_ASSOCIATED, allSourcesVerified);
+ result.putStringArrayList(FAILED_SOURCES, failedSources);
+ mResultReceiver.send(RESULT_SUCCESS, result);
+ return null;
+ }
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java b/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java
new file mode 100644
index 0000000..20c7f97
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/ExceptionLoggingFutureTask.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 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.statementservice;
+
+import android.util.Log;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * {@link FutureTask} that logs unhandled exceptions.
+ */
+final class ExceptionLoggingFutureTask<V> extends FutureTask<V> {
+
+ private final String mTag;
+
+ public ExceptionLoggingFutureTask(Callable<V> callable, String tag) {
+ super(callable);
+ mTag = tag;
+ }
+
+ @Override
+ protected void done() {
+ try {
+ get();
+ } catch (ExecutionException | InterruptedException e) {
+ Log.e(mTag, "Uncaught exception.", e);
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java b/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java
new file mode 100644
index 0000000..712347a
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/IntentFilterVerificationReceiver.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2015 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.statementservice;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.util.Patterns;
+
+import com.android.statementservice.retriever.Utils;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Receives {@link Intent#ACTION_INTENT_FILTER_NEEDS_VERIFICATION} broadcast and calls
+ * {@link DirectStatementService} to verify the request. Calls
+ * {@link PackageManager#verifyIntentFilter} to notify {@link PackageManager} the result of the
+ * verification.
+ *
+ * This implementation of the API will send a HTTP request for each host specified in the query.
+ * To avoid overwhelming the network at app install time, {@code MAX_HOSTS_PER_REQUEST} limits
+ * the maximum number of hosts in a query. If a query contains more than
+ * {@code MAX_HOSTS_PER_REQUEST} hosts, it will fail immediately without making any HTTP request
+ * and call {@link PackageManager#verifyIntentFilter} with
+ * {@link PackageManager#INTENT_FILTER_VERIFICATION_FAILURE}.
+ */
+public final class IntentFilterVerificationReceiver extends BroadcastReceiver {
+ private static final String TAG = IntentFilterVerificationReceiver.class.getSimpleName();
+
+ private static final Integer MAX_HOSTS_PER_REQUEST = 10;
+
+ private static final String HANDLE_ALL_URLS_RELATION
+ = "delegate_permission/common.handle_all_urls";
+
+ private static final String ANDROID_ASSET_FORMAT = "{\"namespace\": \"android_app\", "
+ + "\"package_name\": \"%s\", \"sha256_cert_fingerprints\": [\"%s\"]}";
+ private static final String WEB_ASSET_FORMAT = "{\"namespace\": \"web\", \"site\": \"%s\"}";
+ private static final Pattern ANDROID_PACKAGE_NAME_PATTERN =
+ Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)*$");
+ private static final String TOO_MANY_HOSTS_FORMAT =
+ "Request contains %d hosts which is more than the allowed %d.";
+
+ private static void sendErrorToPackageManager(PackageManager packageManager,
+ int verificationId) {
+ packageManager.verifyIntentFilter(verificationId,
+ PackageManager.INTENT_FILTER_VERIFICATION_FAILURE,
+ Collections.<String>emptyList());
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION.equals(action)) {
+ Bundle inputExtras = intent.getExtras();
+ if (inputExtras != null) {
+ Intent serviceIntent = new Intent(context, DirectStatementService.class);
+ serviceIntent.setAction(DirectStatementService.CHECK_ALL_ACTION);
+
+ int verificationId = inputExtras.getInt(
+ PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID);
+ String scheme = inputExtras.getString(
+ PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME);
+ String hosts = inputExtras.getString(
+ PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS);
+ String packageName = inputExtras.getString(
+ PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME);
+
+ Log.i(TAG, "Verify IntentFilter for " + hosts);
+
+ Bundle extras = new Bundle();
+ extras.putString(DirectStatementService.EXTRA_RELATION, HANDLE_ALL_URLS_RELATION);
+
+ String[] hostList = hosts.split(" ");
+ if (hostList.length > MAX_HOSTS_PER_REQUEST) {
+ Log.w(TAG, String.format(TOO_MANY_HOSTS_FORMAT,
+ hostList.length, MAX_HOSTS_PER_REQUEST));
+ sendErrorToPackageManager(context.getPackageManager(), verificationId);
+ return;
+ }
+
+ try {
+ ArrayList<String> sourceAssets = new ArrayList<String>();
+ for (String host : hostList) {
+ sourceAssets.add(createWebAssetString(scheme, host));
+ }
+ extras.putStringArrayList(DirectStatementService.EXTRA_SOURCE_ASSET_DESCRIPTORS,
+ sourceAssets);
+ } catch (MalformedURLException e) {
+ Log.w(TAG, "Error when processing input host: " + e.getMessage());
+ sendErrorToPackageManager(context.getPackageManager(), verificationId);
+ return;
+ }
+ try {
+ extras.putString(DirectStatementService.EXTRA_TARGET_ASSET_DESCRIPTOR,
+ createAndroidAssetString(context, packageName));
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Error when processing input Android package: " + e.getMessage());
+ sendErrorToPackageManager(context.getPackageManager(), verificationId);
+ return;
+ }
+ extras.putParcelable(DirectStatementService.EXTRA_RESULT_RECEIVER,
+ new IsAssociatedResultReceiver(
+ new Handler(), context.getPackageManager(), verificationId));
+
+ serviceIntent.putExtras(extras);
+ context.startService(serviceIntent);
+ }
+ } else {
+ Log.w(TAG, "Intent action not supported: " + action);
+ }
+ }
+
+ private String createAndroidAssetString(Context context, String packageName)
+ throws NameNotFoundException {
+ if (!ANDROID_PACKAGE_NAME_PATTERN.matcher(packageName).matches()) {
+ throw new NameNotFoundException("Input package name is not valid.");
+ }
+
+ List<String> certFingerprints =
+ Utils.getCertFingerprintsFromPackageManager(packageName, context);
+
+ return String.format(ANDROID_ASSET_FORMAT, packageName,
+ Utils.joinStrings("\", \"", certFingerprints));
+ }
+
+ private String createWebAssetString(String scheme, String host) throws MalformedURLException {
+ if (!Patterns.DOMAIN_NAME.matcher(host).matches()) {
+ throw new MalformedURLException("Input host is not valid.");
+ }
+ if (!scheme.equals("http") && !scheme.equals("https")) {
+ throw new MalformedURLException("Input scheme is not valid.");
+ }
+
+ return String.format(WEB_ASSET_FORMAT, new URL(scheme, host, "").toString());
+ }
+
+ /**
+ * Receives the result of {@code StatementService.CHECK_ACTION} from
+ * {@link DirectStatementService} and passes it back to {@link PackageManager}.
+ */
+ private static class IsAssociatedResultReceiver extends ResultReceiver {
+
+ private final int mVerificationId;
+ private final PackageManager mPackageManager;
+
+ public IsAssociatedResultReceiver(Handler handler, PackageManager packageManager,
+ int verificationId) {
+ super(handler);
+ mVerificationId = verificationId;
+ mPackageManager = packageManager;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == DirectStatementService.RESULT_SUCCESS) {
+ if (resultData.getBoolean(DirectStatementService.IS_ASSOCIATED)) {
+ mPackageManager.verifyIntentFilter(mVerificationId,
+ PackageManager.INTENT_FILTER_VERIFICATION_SUCCESS,
+ Collections.<String>emptyList());
+ } else {
+ mPackageManager.verifyIntentFilter(mVerificationId,
+ PackageManager.INTENT_FILTER_VERIFICATION_FAILURE,
+ resultData.getStringArrayList(DirectStatementService.FAILED_SOURCES));
+ }
+ } else {
+ sendErrorToPackageManager(mPackageManager, mVerificationId);
+ }
+ }
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
new file mode 100644
index 0000000..e71cf54
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAsset.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+/**
+ * A handle representing the identity and address of some digital asset. An asset is an online
+ * entity that typically provides some service or content. Examples of assets are websites, Android
+ * apps, Twitter feeds, and Plus Pages.
+ *
+ * <p> Asset can be represented by a JSON string. For example, the web site https://www.google.com
+ * can be represented by
+ * <pre>
+ * {"namespace": "web", "site": "https://www.google.com"}
+ * </pre>
+ *
+ * <p> The Android app with package name com.google.test that is signed by a certificate with sha256
+ * fingerprint 11:22:33 can be represented by
+ * <pre>
+ * {"namespace": "android_app",
+ * "package_name": "com.google.test",
+ * "sha256_cert_fingerprints": ["11:22:33"]}
+ * </pre>
+ *
+ * <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
+ * {@code keytool -list -printcert -jarfile signed_app.apk}
+ */
+public abstract class AbstractAsset {
+
+ /**
+ * Returns a JSON string representation of this asset. The strings returned by this function are
+ * normalized -- they can be used for equality testing.
+ */
+ public abstract String toJson();
+
+ /**
+ * Returns a key that can be used by {@link AbstractAssetMatcher} to lookup the asset.
+ *
+ * <p> An asset will match an {@code AssetMatcher} only if the value of this method is equal to
+ * {@code AssetMatcher.getMatchedLookupKey()}.
+ */
+ public abstract int lookupKey();
+
+ /**
+ * Creates a new Asset from its JSON string representation.
+ *
+ * @throws AssociationServiceException if the assetJson is not well formatted.
+ */
+ public static AbstractAsset create(String assetJson)
+ throws AssociationServiceException {
+ return AssetFactory.create(assetJson);
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractAssetMatcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAssetMatcher.java
new file mode 100644
index 0000000..c35553f
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AbstractAssetMatcher.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import org.json.JSONException;
+
+/**
+ * An asset matcher that can match asset with the given query.
+ */
+public abstract class AbstractAssetMatcher {
+
+ /**
+ * Returns true if this AssetMatcher matches the asset.
+ */
+ public abstract boolean matches(AbstractAsset asset);
+
+ /**
+ * This AssetMatcher will only match Asset with {@code lookupKey()} equal to the value returned
+ * by this method.
+ */
+ public abstract int getMatchedLookupKey();
+
+ /**
+ * Creates a new AssetMatcher from its JSON string representation.
+ *
+ * <p> For web namespace, {@code query} will match assets that have the same 'site' field.
+ *
+ * <p> For Android namespace, {@code query} will match assets that have the same
+ * 'package_name' field and have at least one common certificate fingerprint in
+ * 'sha256_cert_fingerprints' field.
+ */
+ public static AbstractAssetMatcher createMatcher(String query)
+ throws AssociationServiceException, JSONException {
+ return AssetMatcherFactory.create(query);
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java b/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java
new file mode 100644
index 0000000..fb30bc1
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AbstractStatementRetriever.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import android.content.Context;
+import android.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Retrieves the statements made by assets. This class is the entry point of the package.
+ * <p>
+ * An asset is an identifiable and addressable online entity that typically
+ * provides some service or content. Examples of assets are websites, Android
+ * apps, Twitter feeds, and Plus Pages.
+ * <p>
+ * Ownership of an asset is defined by being able to control it and speak for it.
+ * An asset owner may establish a relationship between the asset and another
+ * asset by making a statement about an intended relationship between the two.
+ * An example of a relationship is permission delegation. For example, the owner
+ * of a website (the webmaster) may delegate the ability the handle URLs to a
+ * particular mobile app. Relationships are considered public information.
+ * <p>
+ * A particular kind of relationship (like permission delegation) defines a binary
+ * relation on assets. The relation is not symmetric or transitive, nor is it
+ * antisymmetric or anti-transitive.
+ * <p>
+ * A statement S(r, a, b) is an assertion that the relation r holds for the
+ * ordered pair of assets (a, b). For example, taking r = "delegates permission
+ * to view user's location", a = New York Times mobile app,
+ * b = nytimes.com website, S(r, a, b) would be an assertion that "the New York
+ * Times mobile app delegates its ability to use the user's location to the
+ * nytimes.com website".
+ * <p>
+ * A statement S(r, a, b) is considered <b>reliable</b> if we have confidence that
+ * the statement is true; the exact criterion depends on the kind of statement,
+ * since some kinds of statements may be true on their face whereas others may
+ * require multiple parties to agree.
+ * <p>
+ * For example, to get the statements made by www.example.com use:
+ * <pre>
+ * result = retrieveStatements(AssetFactory.create(
+ * "{\"namespace\": \"web\", \"site\": \"https://www.google.com\"}"))
+ * </pre>
+ * {@code result} will contain the statements and the expiration time of this result. The statements
+ * are considered reliable until the expiration time.
+ */
+public abstract class AbstractStatementRetriever {
+
+ /**
+ * Returns the statements made by the {@code source} asset with ttl.
+ *
+ * @throws AssociationServiceException if the asset namespace is not supported.
+ */
+ public abstract Result retrieveStatements(AbstractAsset source)
+ throws AssociationServiceException;
+
+ /**
+ * The retrieved statements and the expiration date.
+ */
+ public interface Result {
+
+ /**
+ * @return the retrieved statements.
+ */
+ @NonNull
+ public List<Statement> getStatements();
+
+ /**
+ * @return the expiration time in millisecond.
+ */
+ public long getExpireMillis();
+ }
+
+ /**
+ * Creates a new StatementRetriever that directly retrieves statements from the asset.
+ *
+ * <p> For web assets, {@link AbstractStatementRetriever} will try to retrieve the statement
+ * file from URL: {@code [webAsset.site]/.well-known/associations.json"} where {@code
+ * [webAsset.site]} is in the form {@code http{s}://[hostname]:[optional_port]}. The file
+ * should contain one JSON array of statements.
+ *
+ * <p> For Android assets, {@link AbstractStatementRetriever} will try to retrieve the statement
+ * from the AndroidManifest.xml. The developer should add a {@code meta-data} tag under
+ * {@code application} tag where attribute {@code android:name} equals "associated_assets"
+ * and {@code android:recourse} points to a string array resource. Each entry in the string
+ * array should contain exactly one statement in JSON format. Note that this implementation
+ * can only return statements made by installed apps.
+ */
+ public static AbstractStatementRetriever createDirectRetriever(Context context) {
+ return new DirectStatementRetriever(new URLFetcher(),
+ new AndroidPackageInfoFetcher(context));
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java
new file mode 100644
index 0000000..0c96038
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAsset.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Immutable value type that names an Android app asset.
+ *
+ * <p>An Android app can be named by its package name and certificate fingerprints using this JSON
+ * string: { "namespace": "android_app", "package_name": "[Java package name]",
+ * "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] }
+ *
+ * <p>For example, { "namespace": "android_app", "package_name": "com.test.mytestapp",
+ * "sha256_cert_fingerprints": ["24:D9:B4:57:A6:42:FB:E6:E5:B8:D6:9E:7B:2D:C2:D1:CB:D1:77:17:1D:7F:D4:A9:16:10:11:AB:92:B9:8F:3F"]
+ * }
+ *
+ * <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
+ * {@code keytool -list -printcert -jarfile signed_app.apk}
+ *
+ * <p>Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...)
+ * representing the certificate SHA-256 fingerprint.
+ */
+/* package private */ final class AndroidAppAsset extends AbstractAsset {
+
+ private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
+ private static final String MISSING_APPCERTS_FORMAT_STRING =
+ "Expected %s to be non-empty array.";
+ private static final String APPCERT_NOT_STRING_FORMAT_STRING = "Expected all %s to be strings.";
+
+ private final List<String> mCertFingerprints;
+ private final String mPackageName;
+
+ public List<String> getCertFingerprints() {
+ return Collections.unmodifiableList(mCertFingerprints);
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ @Override
+ public String toJson() {
+ AssetJsonWriter writer = new AssetJsonWriter();
+
+ writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_ANDROID_APP);
+ writer.writeFieldLower(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName);
+ writer.writeArrayUpper(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints);
+
+ return writer.closeAndGetString();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder asset = new StringBuilder();
+ asset.append("AndroidAppAsset: ");
+ asset.append(toJson());
+ return asset.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof AndroidAppAsset)) {
+ return false;
+ }
+
+ return ((AndroidAppAsset) o).toJson().equals(toJson());
+ }
+
+ @Override
+ public int hashCode() {
+ return toJson().hashCode();
+ }
+
+ @Override
+ public int lookupKey() {
+ return getPackageName().hashCode();
+ }
+
+ /**
+ * Checks that the input is a valid Android app asset.
+ *
+ * @param asset a JSONObject that has "namespace", "package_name", and
+ * "sha256_cert_fingerprints" fields.
+ * @throws AssociationServiceException if the asset is not well formatted.
+ */
+ public static AndroidAppAsset create(JSONObject asset)
+ throws AssociationServiceException {
+ String packageName = asset.optString(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME);
+ if (packageName.equals("")) {
+ throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
+ Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME));
+ }
+
+ JSONArray certArray = asset.optJSONArray(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS);
+ if (certArray == null || certArray.length() == 0) {
+ throw new AssociationServiceException(
+ String.format(MISSING_APPCERTS_FORMAT_STRING,
+ Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
+ }
+ List<String> certFingerprints = new ArrayList<>(certArray.length());
+ for (int i = 0; i < certArray.length(); i++) {
+ try {
+ certFingerprints.add(certArray.getString(i));
+ } catch (JSONException e) {
+ throw new AssociationServiceException(
+ String.format(APPCERT_NOT_STRING_FORMAT_STRING,
+ Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
+ }
+ }
+
+ return new AndroidAppAsset(packageName, certFingerprints);
+ }
+
+ /**
+ * Creates a new AndroidAppAsset.
+ *
+ * @param packageName the package name of the Android app.
+ * @param certFingerprints at least one of the Android app signing certificate sha-256
+ * fingerprint.
+ */
+ public static AndroidAppAsset create(String packageName, List<String> certFingerprints) {
+ if (packageName == null || packageName.equals("")) {
+ throw new AssertionError("Expected packageName to be set.");
+ }
+ if (certFingerprints == null || certFingerprints.size() == 0) {
+ throw new AssertionError("Expected certFingerprints to be set.");
+ }
+ List<String> lowerFps = new ArrayList<String>(certFingerprints.size());
+ for (String fp : certFingerprints) {
+ lowerFps.add(fp.toUpperCase(Locale.US));
+ }
+ return new AndroidAppAsset(packageName, lowerFps);
+ }
+
+ private AndroidAppAsset(String packageName, List<String> certFingerprints) {
+ if (packageName.equals("")) {
+ mPackageName = null;
+ } else {
+ mPackageName = packageName;
+ }
+
+ if (certFingerprints == null || certFingerprints.size() == 0) {
+ mCertFingerprints = null;
+ } else {
+ mCertFingerprints = Collections.unmodifiableList(sortAndDeDuplicate(certFingerprints));
+ }
+ }
+
+ /**
+ * Returns an ASCII-sorted copy of the list of certs with all duplicates removed.
+ */
+ private List<String> sortAndDeDuplicate(List<String> certs) {
+ if (certs.size() <= 1) {
+ return certs;
+ }
+ HashSet<String> set = new HashSet<>(certs);
+ List<String> result = new ArrayList<>(set);
+ Collections.sort(result);
+ return result;
+ }
+
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java
new file mode 100644
index 0000000..8a9d838
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidAppAssetMatcher.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Match assets that have the same 'package_name' field and have at least one common certificate
+ * fingerprint in 'sha256_cert_fingerprints' field.
+ */
+/* package private */ final class AndroidAppAssetMatcher extends AbstractAssetMatcher {
+
+ private final AndroidAppAsset mQuery;
+
+ public AndroidAppAssetMatcher(AndroidAppAsset query) {
+ mQuery = query;
+ }
+
+ @Override
+ public boolean matches(AbstractAsset asset) {
+ if (asset instanceof AndroidAppAsset) {
+ AndroidAppAsset androidAppAsset = (AndroidAppAsset) asset;
+ if (!androidAppAsset.getPackageName().equals(mQuery.getPackageName())) {
+ return false;
+ }
+
+ Set<String> certs = new HashSet<String>(mQuery.getCertFingerprints());
+ for (String cert : androidAppAsset.getCertFingerprints()) {
+ if (certs.contains(cert)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int getMatchedLookupKey() {
+ return mQuery.lookupKey();
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java b/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java
new file mode 100644
index 0000000..1000c4c
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AndroidPackageInfoFetcher.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources.NotFoundException;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Class that provides information about an android app from {@link PackageManager}.
+ *
+ * Visible for testing.
+ *
+ * @hide
+ */
+public class AndroidPackageInfoFetcher {
+
+ /**
+ * The name of the metadata tag in AndroidManifest.xml that stores the associated asset array
+ * ID. The metadata tag should use the android:resource attribute to point to an array resource
+ * that contains the associated assets.
+ */
+ private static final String ASSOCIATED_ASSETS_KEY = "associated_assets";
+
+ private Context mContext;
+
+ public AndroidPackageInfoFetcher(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Returns the Sha-256 fingerprints of all certificates from the specified package as a list of
+ * upper case HEX Strings with bytes separated by colons. Given an app {@link
+ * android.content.pm.Signature}, the fingerprint can be computed as {@link
+ * Utils#computeNormalizedSha256Fingerprint} {@code(signature.toByteArray())}.
+ *
+ * <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using: {@code
+ * keytool -list -printcert -jarfile signed_app.apk}
+ *
+ * <p>Example: "10:39:38:EE:45:37:E5:9E:8E:E7:92:F6:54:50:4F:B8:34:6F:C6:B3:46:D0:BB:C4:41:5F:C3:39:FC:FC:8E:C1"
+ *
+ * @throws NameNotFoundException if an app with packageName is not installed on the device.
+ */
+ public List<String> getCertFingerprints(String packageName) throws NameNotFoundException {
+ return Utils.getCertFingerprintsFromPackageManager(packageName, mContext);
+ }
+
+ /**
+ * Returns all statements that the specified package makes in its AndroidManifest.xml.
+ *
+ * @throws NameNotFoundException if the app is not installed on the device.
+ */
+ public List<String> getStatements(String packageName) throws NameNotFoundException {
+ PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(
+ packageName, PackageManager.GET_META_DATA);
+ ApplicationInfo appInfo = packageInfo.applicationInfo;
+ if (appInfo.metaData == null) {
+ return Collections.<String>emptyList();
+ }
+ int tokenResourceId = appInfo.metaData.getInt(ASSOCIATED_ASSETS_KEY);
+ if (tokenResourceId == 0) {
+ return Collections.<String>emptyList();
+ }
+ try {
+ return Arrays.asList(
+ mContext.getPackageManager().getResourcesForApplication(packageName)
+ .getStringArray(tokenResourceId));
+ } catch (NotFoundException e) {
+ return Collections.<String>emptyList();
+ }
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java
new file mode 100644
index 0000000..762365e
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Factory to create asset from JSON string.
+ */
+/* package private */ final class AssetFactory {
+
+ private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string.";
+
+ private AssetFactory() {}
+
+ /**
+ * Creates a new Asset object from its JSON string representation.
+ *
+ * @throws AssociationServiceException if the assetJson is not well formatted.
+ */
+ public static AbstractAsset create(String assetJson) throws AssociationServiceException {
+ try {
+ return create(new JSONObject(assetJson));
+ } catch (JSONException e) {
+ throw new AssociationServiceException(
+ "Input is not a well formatted asset descriptor.");
+ }
+ }
+
+ /**
+ * Checks that the input is a valid asset with purposes.
+ *
+ * @throws AssociationServiceException if the asset is not well formatted.
+ */
+ private static AbstractAsset create(JSONObject asset)
+ throws AssociationServiceException {
+ String namespace = asset.optString(Utils.NAMESPACE_FIELD, null);
+ if (namespace == null) {
+ throw new AssociationServiceException(String.format(
+ FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD));
+ }
+
+ if (namespace.equals(Utils.NAMESPACE_WEB)) {
+ return WebAsset.create(asset);
+ } else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) {
+ return AndroidAppAsset.create(asset);
+ } else {
+ throw new AssociationServiceException("Namespace " + namespace + " is not supported.");
+ }
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssetJsonWriter.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetJsonWriter.java
new file mode 100644
index 0000000..080e45a
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetJsonWriter.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import android.util.JsonWriter;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Creates a Json string where the order of the fields can be specified.
+ */
+/* package private */ final class AssetJsonWriter {
+
+ private StringWriter mStringWriter = new StringWriter();
+ private JsonWriter mWriter;
+ private boolean mClosed = false;
+
+ public AssetJsonWriter() {
+ mWriter = new JsonWriter(mStringWriter);
+ try {
+ mWriter.beginObject();
+ } catch (IOException e) {
+ throw new AssertionError("Unreachable exception.");
+ }
+ }
+
+ /**
+ * Appends a field to the output, putting both the key and value in lowercase. Null values are
+ * not written.
+ */
+ public void writeFieldLower(String key, String value) {
+ if (mClosed) {
+ throw new IllegalArgumentException(
+ "Cannot write to an object that has already been closed.");
+ }
+
+ if (value != null) {
+ try {
+ mWriter.name(key.toLowerCase(Locale.US));
+ mWriter.value(value.toLowerCase(Locale.US));
+ } catch (IOException e) {
+ throw new AssertionError("Unreachable exception.");
+ }
+ }
+ }
+
+ /**
+ * Appends an array to the output, putting both the key and values in lowercase. If {@code
+ * values} is null, this field will not be written. Individual values in the list must not be
+ * null.
+ */
+ public void writeArrayUpper(String key, List<String> values) {
+ if (mClosed) {
+ throw new IllegalArgumentException(
+ "Cannot write to an object that has already been closed.");
+ }
+
+ if (values != null) {
+ try {
+ mWriter.name(key.toLowerCase(Locale.US));
+ mWriter.beginArray();
+ for (String value : values) {
+ mWriter.value(value.toUpperCase(Locale.US));
+ }
+ mWriter.endArray();
+ } catch (IOException e) {
+ throw new AssertionError("Unreachable exception.");
+ }
+ }
+ }
+
+ /**
+ * Returns the string representation of the constructed json. After calling this method, {@link
+ * #writeFieldLower} can no longer be called.
+ */
+ public String closeAndGetString() {
+ if (!mClosed) {
+ try {
+ mWriter.endObject();
+ } catch (IOException e) {
+ throw new AssertionError("Unreachable exception.");
+ }
+ mClosed = true;
+ }
+ return mStringWriter.toString();
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java b/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java
new file mode 100644
index 0000000..1a50757
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssetMatcherFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Factory to create asset matcher from JSON string.
+ */
+/* package private */ final class AssetMatcherFactory {
+
+ private static final String FIELD_NOT_STRING_FORMAT_STRING = "Expected %s to be string.";
+ private static final String NAMESPACE_NOT_SUPPORTED_STRING = "Namespace %s is not supported.";
+
+ public static AbstractAssetMatcher create(String query) throws AssociationServiceException,
+ JSONException {
+ JSONObject queryObject = new JSONObject(query);
+
+ String namespace = queryObject.optString(Utils.NAMESPACE_FIELD, null);
+ if (namespace == null) {
+ throw new AssociationServiceException(String.format(
+ FIELD_NOT_STRING_FORMAT_STRING, Utils.NAMESPACE_FIELD));
+ }
+
+ if (namespace.equals(Utils.NAMESPACE_WEB)) {
+ return new WebAssetMatcher(WebAsset.create(queryObject));
+ } else if (namespace.equals(Utils.NAMESPACE_ANDROID_APP)) {
+ return new AndroidAppAssetMatcher(AndroidAppAsset.create(queryObject));
+ } else {
+ throw new AssociationServiceException(
+ String.format(NAMESPACE_NOT_SUPPORTED_STRING, namespace));
+ }
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/AssociationServiceException.java b/packages/StatementService/src/com/android/statementservice/retriever/AssociationServiceException.java
new file mode 100644
index 0000000..d6e49c2
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/AssociationServiceException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+/**
+ * Thrown when an error occurs in the Association Service.
+ */
+public class AssociationServiceException extends Exception {
+
+ public AssociationServiceException(String msg) {
+ super(msg);
+ }
+
+ public AssociationServiceException(String msg, Exception e) {
+ super(msg, e);
+ }
+
+ public AssociationServiceException(Exception e) {
+ super(e);
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java b/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java
new file mode 100644
index 0000000..3ad71c4
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/DirectStatementRetriever.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
+
+import org.json.JSONException;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An implementation of {@link AbstractStatementRetriever} that directly retrieves statements from
+ * the asset.
+ */
+/* package private */ final class DirectStatementRetriever extends AbstractStatementRetriever {
+
+ private static final long DO_NOT_CACHE_RESULT = 0L;
+ private static final int HTTP_CONNECTION_TIMEOUT_MILLIS = 5000;
+ private static final long HTTP_CONTENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
+ private static final int MAX_INCLUDE_LEVEL = 1;
+ private static final String WELL_KNOWN_STATEMENT_PATH = "/.well-known/associations.json";
+
+ private final URLFetcher mUrlFetcher;
+ private final AndroidPackageInfoFetcher mAndroidFetcher;
+
+ /**
+ * An immutable value type representing the retrieved statements and the expiration date.
+ */
+ public static class Result implements AbstractStatementRetriever.Result {
+
+ private final List<Statement> mStatements;
+ private final Long mExpireMillis;
+
+ @Override
+ public List<Statement> getStatements() {
+ return mStatements;
+ }
+
+ @Override
+ public long getExpireMillis() {
+ return mExpireMillis;
+ }
+
+ private Result(List<Statement> statements, Long expireMillis) {
+ mStatements = statements;
+ mExpireMillis = expireMillis;
+ }
+
+ public static Result create(List<Statement> statements, Long expireMillis) {
+ return new Result(statements, expireMillis);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("Result: ");
+ result.append(mStatements.toString());
+ result.append(", mExpireMillis=");
+ result.append(mExpireMillis);
+ return result.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Result result = (Result) o;
+
+ if (!mExpireMillis.equals(result.mExpireMillis)) {
+ return false;
+ }
+ if (!mStatements.equals(result.mStatements)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mStatements.hashCode();
+ result = 31 * result + mExpireMillis.hashCode();
+ return result;
+ }
+ }
+
+ public DirectStatementRetriever(URLFetcher urlFetcher,
+ AndroidPackageInfoFetcher androidFetcher) {
+ this.mUrlFetcher = urlFetcher;
+ this.mAndroidFetcher = androidFetcher;
+ }
+
+ @Override
+ public Result retrieveStatements(AbstractAsset source) throws AssociationServiceException {
+ if (source instanceof AndroidAppAsset) {
+ return retrieveFromAndroid((AndroidAppAsset) source);
+ } else if (source instanceof WebAsset) {
+ return retrieveFromWeb((WebAsset) source);
+ } else {
+ throw new AssociationServiceException("Namespace is not supported.");
+ }
+ }
+
+ private String computeAssociationJsonUrl(WebAsset asset) {
+ try {
+ return new URL(asset.getScheme(), asset.getDomain(), asset.getPort(),
+ WELL_KNOWN_STATEMENT_PATH)
+ .toExternalForm();
+ } catch (MalformedURLException e) {
+ throw new AssertionError("Invalid domain name in database.");
+ }
+ }
+
+ private Result retrieveStatementFromUrl(String url, int maxIncludeLevel, AbstractAsset source)
+ throws AssociationServiceException {
+ List<Statement> statements = new ArrayList<Statement>();
+ if (maxIncludeLevel < 0) {
+ return Result.create(statements, DO_NOT_CACHE_RESULT);
+ }
+
+ WebContent webContent;
+ try {
+ webContent = mUrlFetcher.getWebContentFromUrl(new URL(url),
+ HTTP_CONTENT_SIZE_LIMIT_IN_BYTES, HTTP_CONNECTION_TIMEOUT_MILLIS);
+ } catch (IOException e) {
+ return Result.create(statements, DO_NOT_CACHE_RESULT);
+ }
+
+ try {
+ ParsedStatement result = StatementParser
+ .parseStatementList(webContent.getContent(), source);
+ statements.addAll(result.getStatements());
+ for (String delegate : result.getDelegates()) {
+ statements.addAll(
+ retrieveStatementFromUrl(delegate, maxIncludeLevel - 1, source)
+ .getStatements());
+ }
+ return Result.create(statements, webContent.getExpireTimeMillis());
+ } catch (JSONException e) {
+ return Result.create(statements, DO_NOT_CACHE_RESULT);
+ }
+ }
+
+ private Result retrieveFromWeb(WebAsset asset)
+ throws AssociationServiceException {
+ return retrieveStatementFromUrl(computeAssociationJsonUrl(asset), MAX_INCLUDE_LEVEL, asset);
+ }
+
+ private Result retrieveFromAndroid(AndroidAppAsset asset) throws AssociationServiceException {
+ try {
+ List<String> delegates = new ArrayList<String>();
+ List<Statement> statements = new ArrayList<Statement>();
+
+ List<String> certFps = mAndroidFetcher.getCertFingerprints(asset.getPackageName());
+ if (!Utils.hasCommonString(certFps, asset.getCertFingerprints())) {
+ throw new AssociationServiceException(
+ "Specified certs don't match the installed app.");
+ }
+
+ AndroidAppAsset actualSource = AndroidAppAsset.create(asset.getPackageName(), certFps);
+ for (String statementJson : mAndroidFetcher.getStatements(asset.getPackageName())) {
+ ParsedStatement result =
+ StatementParser.parseStatement(statementJson, actualSource);
+ statements.addAll(result.getStatements());
+ delegates.addAll(result.getDelegates());
+ }
+
+ for (String delegate : delegates) {
+ statements.addAll(retrieveStatementFromUrl(delegate, MAX_INCLUDE_LEVEL,
+ actualSource).getStatements());
+ }
+
+ return Result.create(statements, DO_NOT_CACHE_RESULT);
+ } catch (JSONException | NameNotFoundException e) {
+ Log.w(DirectStatementRetriever.class.getSimpleName(), e);
+ return Result.create(Collections.<Statement>emptyList(), DO_NOT_CACHE_RESULT);
+ }
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java b/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java
new file mode 100644
index 0000000..9446e66
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/ParsedStatement.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import java.util.List;
+
+/**
+ * A class that stores a list of statement and/or a list of delegate url.
+ */
+/* package private */ final class ParsedStatement {
+
+ private final List<Statement> mStatements;
+ private final List<String> mDelegates;
+
+ public ParsedStatement(List<Statement> statements, List<String> delegates) {
+ this.mStatements = statements;
+ this.mDelegates = delegates;
+ }
+
+ public List<Statement> getStatements() {
+ return mStatements;
+ }
+
+ public List<String> getDelegates() {
+ return mDelegates;
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Relation.java b/packages/StatementService/src/com/android/statementservice/retriever/Relation.java
new file mode 100644
index 0000000..91218c6
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/Relation.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import android.annotation.NonNull;
+
+import java.util.regex.Pattern;
+
+/**
+ * An immutable value type representing a statement relation with "kind" and "detail".
+ *
+ * <p> The set of kinds is enumerated by the API: <ul> <li> <b>delegate_permission</b>: The detail
+ * field specifies which permission to delegate. A statement involving this relation does not
+ * constitute a requirement to do the delegation, just a permission to do so. </ul>
+ *
+ * <p> We may add other kinds in the future.
+ *
+ * <p> The detail field is a lowercase alphanumeric string with underscores and periods allowed
+ * (matching the regex [a-z0-9_.]+), but otherwise unstructured. It is also possible to specify '*'
+ * (the wildcard character) as the detail if the relation applies to any detail in the specified
+ * kind.
+ */
+public final class Relation {
+
+ private static final Pattern KIND_PATTERN = Pattern.compile("^[a-z0-9_.]+$");
+ private static final Pattern DETAIL_PATTERN = Pattern.compile("^([a-z0-9_.]+|[*])$");
+
+ private static final String MATCH_ALL_DETAILS = "*";
+
+ private final String mKind;
+ private final String mDetail;
+
+ private Relation(String kind, String detail) {
+ mKind = kind;
+ mDetail = detail;
+ }
+
+ /**
+ * Returns the relation's kind.
+ */
+ @NonNull
+ public String getKind() {
+ return mKind;
+ }
+
+ /**
+ * Returns the relation's detail.
+ */
+ @NonNull
+ public String getDetail() {
+ return mDetail;
+ }
+
+ /**
+ * Creates a new Relation object for the specified {@code kind} and {@code detail}.
+ *
+ * @throws AssociationServiceException if {@code kind} or {@code detail} is not well formatted.
+ */
+ public static Relation create(@NonNull String kind, @NonNull String detail)
+ throws AssociationServiceException {
+ if (!KIND_PATTERN.matcher(kind).matches() || !DETAIL_PATTERN.matcher(detail).matches()) {
+ throw new AssociationServiceException("Relation not well formatted.");
+ }
+ return new Relation(kind, detail);
+ }
+
+ /**
+ * Creates a new Relation object from its string representation.
+ *
+ * @throws AssociationServiceException if the relation is not well formatted.
+ */
+ public static Relation create(@NonNull String relation) throws AssociationServiceException {
+ String[] r = relation.split("/", 2);
+ if (r.length != 2) {
+ throw new AssociationServiceException("Relation not well formatted.");
+ }
+ return create(r[0], r[1]);
+ }
+
+ /**
+ * Returns true if {@code relation} has the same kind and detail. If {@code
+ * relation.getDetail()} is wildcard (*) then returns true if the kind is the same.
+ */
+ public boolean matches(Relation relation) {
+ return getKind().equals(relation.getKind()) && (getDetail().equals(MATCH_ALL_DETAILS)
+ || getDetail().equals(relation.getDetail()));
+ }
+
+ /**
+ * Returns a string representation of this relation.
+ */
+ @Override
+ public String toString() {
+ StringBuilder relation = new StringBuilder();
+ relation.append(getKind());
+ relation.append("/");
+ relation.append(getDetail());
+ return relation.toString();
+ }
+
+ // equals() and hashCode() are generated by Android Studio.
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Relation relation = (Relation) o;
+
+ if (mDetail != null ? !mDetail.equals(relation.mDetail) : relation.mDetail != null) {
+ return false;
+ }
+ if (mKind != null ? !mKind.equals(relation.mKind) : relation.mKind != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mKind != null ? mKind.hashCode() : 0;
+ result = 31 * result + (mDetail != null ? mDetail.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Statement.java b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java
new file mode 100644
index 0000000..f83edaf
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import android.annotation.NonNull;
+
+/**
+ * An immutable value type representing a statement, consisting of a source, target, and relation.
+ * This reflects an assertion that the relation holds for the source, target pair. For example, if a
+ * web site has the following in its associations.json file:
+ *
+ * <pre>
+ * {
+ * "relation": ["delegate_permission/common.handle_all_urls"],
+ * "target" : {"namespace": "android_app", "package_name": "com.example.app",
+ * "sha256_cert_fingerprints": ["00:11:22:33"] }
+ * }
+ * </pre>
+ *
+ * Then invoking {@link AbstractStatementRetriever#retrieveStatements(AbstractAsset)} will return a
+ * {@link Statement} with {@link #getSource} equal to the input parameter, {@link #getRelation}
+ * equal to
+ *
+ * <pre>Relation.create("delegate_permission", "common.get_login_creds");</pre>
+ *
+ * and with {@link #getTarget} equal to
+ *
+ * <pre>AbstractAsset.create("{\"namespace\" : \"android_app\","
+ * + "\"package_name\": \"com.example.app\"}"
+ * + "\"sha256_cert_fingerprints\": \"[\"00:11:22:33\"]\"}");
+ * </pre>
+ */
+public final class Statement {
+
+ private final AbstractAsset mTarget;
+ private final Relation mRelation;
+ private final AbstractAsset mSource;
+
+ private Statement(AbstractAsset source, AbstractAsset target, Relation relation) {
+ mSource = source;
+ mTarget = target;
+ mRelation = relation;
+ }
+
+ /**
+ * Returns the source asset of the statement.
+ */
+ @NonNull
+ public AbstractAsset getSource() {
+ return mSource;
+ }
+
+ /**
+ * Returns the target asset of the statement.
+ */
+ @NonNull
+ public AbstractAsset getTarget() {
+ return mTarget;
+ }
+
+ /**
+ * Returns the relation of the statement.
+ */
+ @NonNull
+ public Relation getRelation() {
+ return mRelation;
+ }
+
+ /**
+ * Creates a new Statement object for the specified target asset and relation. For example:
+ * <pre>
+ * Asset asset = Asset.Factory.create(
+ * "{\"namespace\" : \"web\",\"site\": \"https://www.test.com\"}");
+ * Relation relation = Relation.create("delegate_permission", "common.get_login_creds");
+ * Statement statement = Statement.create(asset, relation);
+ * </pre>
+ */
+ public static Statement create(@NonNull AbstractAsset source, @NonNull AbstractAsset target,
+ @NonNull Relation relation) {
+ return new Statement(source, target, relation);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Statement statement = (Statement) o;
+
+ if (!mRelation.equals(statement.mRelation)) {
+ return false;
+ }
+ if (!mTarget.equals(statement.mTarget)) {
+ return false;
+ }
+ if (!mSource.equals(statement.mSource)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mTarget.hashCode();
+ result = 31 * result + mRelation.hashCode();
+ result = 31 * result + mSource.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder statement = new StringBuilder();
+ statement.append("Statement: ");
+ statement.append(mSource);
+ statement.append(", ");
+ statement.append(mTarget);
+ statement.append(", ");
+ statement.append(mRelation);
+ return statement.toString();
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java b/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java
new file mode 100644
index 0000000..bcd91bd
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/StatementParser.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class that parses JSON-formatted statements.
+ */
+/* package private */ final class StatementParser {
+
+ /**
+ * Parses a JSON array of statements.
+ */
+ static ParsedStatement parseStatementList(String statementList, AbstractAsset source)
+ throws JSONException, AssociationServiceException {
+ List<Statement> statements = new ArrayList<Statement>();
+ List<String> delegates = new ArrayList<String>();
+
+ JSONArray statementsJson = new JSONArray(statementList);
+ for (int i = 0; i < statementsJson.length(); i++) {
+ ParsedStatement result = parseStatement(statementsJson.getString(i), source);
+ statements.addAll(result.getStatements());
+ delegates.addAll(result.getDelegates());
+ }
+
+ return new ParsedStatement(statements, delegates);
+ }
+
+ /**
+ * Parses a single JSON statement.
+ */
+ static ParsedStatement parseStatement(String statementString, AbstractAsset source)
+ throws JSONException, AssociationServiceException {
+ List<Statement> statements = new ArrayList<Statement>();
+ List<String> delegates = new ArrayList<String>();
+ JSONObject statement = new JSONObject(statementString);
+ if (statement.optString(Utils.DELEGATE_FIELD_DELEGATE, null) != null) {
+ delegates.add(statement.optString(Utils.DELEGATE_FIELD_DELEGATE));
+ } else {
+ AbstractAsset target = AssetFactory
+ .create(statement.getString(Utils.ASSET_DESCRIPTOR_FIELD_TARGET));
+ JSONArray relations = statement.getJSONArray(
+ Utils.ASSET_DESCRIPTOR_FIELD_RELATION);
+ for (int i = 0; i < relations.length(); i++) {
+ statements.add(Statement
+ .create(source, target, Relation.create(relations.getString(i))));
+ }
+ }
+
+ return new ParsedStatement(statements, delegates);
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java b/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java
new file mode 100644
index 0000000..4828ff9
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/URLFetcher.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import com.android.volley.Cache;
+import com.android.volley.NetworkResponse;
+import com.android.volley.toolbox.HttpHeaderParser;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Helper class for fetching HTTP or HTTPS URL.
+ *
+ * Visible for testing.
+ *
+ * @hide
+ */
+public class URLFetcher {
+
+ private static final long DO_NOT_CACHE_RESULT = 0L;
+ private static final int INPUT_BUFFER_SIZE_IN_BYTES = 1024;
+
+ /**
+ * Fetches the specified url and returns the content and ttl.
+ *
+ * @throws IOException if it can't retrieve the content due to a network problem.
+ * @throws AssociationServiceException if the URL scheme is not http or https or the content
+ * length exceeds {code fileSizeLimit}.
+ */
+ public WebContent getWebContentFromUrl(URL url, long fileSizeLimit, int connectionTimeoutMillis)
+ throws AssociationServiceException, IOException {
+ final String scheme = url.getProtocol().toLowerCase(Locale.US);
+ if (!scheme.equals("http") && !scheme.equals("https")) {
+ throw new IllegalArgumentException("The url protocol should be on http or https.");
+ }
+
+ HttpURLConnection connection;
+ connection = (HttpURLConnection) url.openConnection();
+ connection.setInstanceFollowRedirects(true);
+ connection.setConnectTimeout(connectionTimeoutMillis);
+ connection.setReadTimeout(connectionTimeoutMillis);
+ connection.setUseCaches(true);
+ connection.addRequestProperty("Cache-Control", "max-stale=60");
+
+ if (connection.getContentLength() > fileSizeLimit) {
+ throw new AssociationServiceException("The content size of the url is larger than "
+ + fileSizeLimit);
+ }
+
+ Long expireTimeMillis = getExpirationTimeMillisFromHTTPHeader(connection.getHeaderFields());
+
+ try {
+ return new WebContent(inputStreamToString(
+ connection.getInputStream(), connection.getContentLength(), fileSizeLimit),
+ expireTimeMillis);
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ /**
+ * Visible for testing.
+ * @hide
+ */
+ public static String inputStreamToString(InputStream inputStream, int length, long sizeLimit)
+ throws IOException, AssociationServiceException {
+ if (length < 0) {
+ length = 0;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(length);
+ BufferedInputStream bis = new BufferedInputStream(inputStream);
+ byte[] buffer = new byte[INPUT_BUFFER_SIZE_IN_BYTES];
+ int len = 0;
+ while ((len = bis.read(buffer)) != -1) {
+ baos.write(buffer, 0, len);
+ if (baos.size() > sizeLimit) {
+ throw new AssociationServiceException("The content size of the url is larger than "
+ + sizeLimit);
+ }
+ }
+ return baos.toString("UTF-8");
+ }
+
+ /**
+ * Parses the HTTP headers to compute the ttl.
+ *
+ * @param headers a map that map the header key to the header values. Can be null.
+ * @return the ttl in millisecond or null if the ttl is not specified in the header.
+ */
+ private Long getExpirationTimeMillisFromHTTPHeader(Map<String, List<String>> headers) {
+ if (headers == null) {
+ return null;
+ }
+ Map<String, String> joinedHeaders = joinHttpHeaders(headers);
+
+ NetworkResponse response = new NetworkResponse(null, joinedHeaders);
+ Cache.Entry cachePolicy = HttpHeaderParser.parseCacheHeaders(response);
+
+ if (cachePolicy == null) {
+ // Cache is disabled, set the expire time to 0.
+ return DO_NOT_CACHE_RESULT;
+ } else if (cachePolicy.ttl == 0) {
+ // Cache policy is not specified, set the expire time to 0.
+ return DO_NOT_CACHE_RESULT;
+ } else {
+ // cachePolicy.ttl is actually the expire timestamp in millisecond.
+ return cachePolicy.ttl;
+ }
+ }
+
+ /**
+ * Converts an HTTP header map of the format provided by {@linkHttpUrlConnection} to a map of
+ * the format accepted by {@link HttpHeaderParser}. It does this by joining all the entries for
+ * a given header key with ", ".
+ */
+ private Map<String, String> joinHttpHeaders(Map<String, List<String>> headers) {
+ Map<String, String> joinedHeaders = new HashMap<String, String>();
+ for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+ List<String> values = entry.getValue();
+ if (values.size() == 1) {
+ joinedHeaders.put(entry.getKey(), values.get(0));
+ } else {
+ joinedHeaders.put(entry.getKey(), Utils.joinStrings(", ", values));
+ }
+ }
+ return joinedHeaders;
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Utils.java b/packages/StatementService/src/com/android/statementservice/retriever/Utils.java
new file mode 100644
index 0000000..44af864
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/Utils.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.Signature;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Utility library for computing certificate fingerprints. Also includes fields name used by
+ * Statement JSON string.
+ */
+public final class Utils {
+
+ private Utils() {}
+
+ /**
+ * Field name for namespace.
+ */
+ public static final String NAMESPACE_FIELD = "namespace";
+
+ /**
+ * Supported asset namespaces.
+ */
+ public static final String NAMESPACE_WEB = "web";
+ public static final String NAMESPACE_ANDROID_APP = "android_app";
+
+ /**
+ * Field names in a web asset descriptor.
+ */
+ public static final String WEB_ASSET_FIELD_SITE = "site";
+
+ /**
+ * Field names in a Android app asset descriptor.
+ */
+ public static final String ANDROID_APP_ASSET_FIELD_PACKAGE_NAME = "package_name";
+ public static final String ANDROID_APP_ASSET_FIELD_CERT_FPS = "sha256_cert_fingerprints";
+
+ /**
+ * Field names in a statement.
+ */
+ public static final String ASSET_DESCRIPTOR_FIELD_RELATION = "relation";
+ public static final String ASSET_DESCRIPTOR_FIELD_TARGET = "target";
+ public static final String DELEGATE_FIELD_DELEGATE = "delegate";
+
+ private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'A', 'B', 'C', 'D', 'E', 'F' };
+
+ /**
+ * Joins a list of strings, by placing separator between each string. For example,
+ * {@code joinStrings("; ", Arrays.asList(new String[]{"a", "b", "c"}))} returns
+ * "{@code a; b; c}".
+ */
+ public static String joinStrings(String separator, List<String> strings) {
+ switch(strings.size()) {
+ case 0:
+ return "";
+ case 1:
+ return strings.get(0);
+ default:
+ StringBuilder joiner = new StringBuilder();
+ boolean first = true;
+ for (String field : strings) {
+ if (first) {
+ first = false;
+ } else {
+ joiner.append(separator);
+ }
+ joiner.append(field);
+ }
+ return joiner.toString();
+ }
+ }
+
+ /**
+ * Returns the normalized sha-256 fingerprints of a given package according to the Android
+ * package manager.
+ */
+ public static List<String> getCertFingerprintsFromPackageManager(String packageName,
+ Context context) throws NameNotFoundException {
+ Signature[] signatures = context.getPackageManager().getPackageInfo(packageName,
+ PackageManager.GET_SIGNATURES).signatures;
+ ArrayList<String> result = new ArrayList<String>(signatures.length);
+ for (Signature sig : signatures) {
+ result.add(computeNormalizedSha256Fingerprint(sig.toByteArray()));
+ }
+ return result;
+ }
+
+ /**
+ * Computes the hash of the byte array using the specified algorithm, returning a hex string
+ * with a colon between each byte.
+ */
+ public static String computeNormalizedSha256Fingerprint(byte[] signature) {
+ MessageDigest digester;
+ try {
+ digester = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError("No SHA-256 implementation found.");
+ }
+ digester.update(signature);
+ return byteArrayToHexString(digester.digest());
+ }
+
+ /**
+ * Returns true if there is at least one common string between the two lists of string.
+ */
+ public static boolean hasCommonString(List<String> list1, List<String> list2) {
+ HashSet<String> set2 = new HashSet<>(list2);
+ for (String string : list1) {
+ if (set2.contains(string)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Converts the byte array to an lowercase hexadecimal digits String with a colon character (:)
+ * between each byte.
+ */
+ private static String byteArrayToHexString(byte[] array) {
+ if (array.length == 0) {
+ return "";
+ }
+ char[] buf = new char[array.length * 3 - 1];
+
+ int bufIndex = 0;
+ for (int i = 0; i < array.length; i++) {
+ byte b = array[i];
+ if (i > 0) {
+ buf[bufIndex++] = ':';
+ }
+ buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
+ buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
+ }
+ return new String(buf);
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java b/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java
new file mode 100644
index 0000000..ca9e62d
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/WebAsset.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+import org.json.JSONObject;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Locale;
+
+/**
+ * Immutable value type that names a web asset.
+ *
+ * <p>A web asset can be named by its protocol, domain, and port using this JSON string:
+ * { "namespace": "web",
+ * "site": "[protocol]://[fully-qualified domain]{:[optional port]}" }
+ *
+ * <p>For example, a website hosted on a https server at www.test.com can be named using
+ * { "namespace": "web",
+ * "site": "https://www.test.com" }
+ *
+ * <p>The only protocol supported now are https and http. If the optional port is not specified,
+ * the default for each protocol will be used (i.e. 80 for http and 443 for https).
+ */
+/* package private */ final class WebAsset extends AbstractAsset {
+
+ private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
+
+ private final URL mUrl;
+
+ private WebAsset(URL url) {
+ int port = url.getPort() != -1 ? url.getPort() : url.getDefaultPort();
+ try {
+ mUrl = new URL(url.getProtocol().toLowerCase(), url.getHost().toLowerCase(), port, "");
+ } catch (MalformedURLException e) {
+ throw new AssertionError(
+ "Url should always be validated before calling the constructor.");
+ }
+ }
+
+ public String getDomain() {
+ return mUrl.getHost();
+ }
+
+ public String getPath() {
+ return mUrl.getPath();
+ }
+
+ public String getScheme() {
+ return mUrl.getProtocol();
+ }
+
+ public int getPort() {
+ return mUrl.getPort();
+ }
+
+ @Override
+ public String toJson() {
+ AssetJsonWriter writer = new AssetJsonWriter();
+
+ writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_WEB);
+ writer.writeFieldLower(Utils.WEB_ASSET_FIELD_SITE, mUrl.toExternalForm());
+
+ return writer.closeAndGetString();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder asset = new StringBuilder();
+ asset.append("WebAsset: ");
+ asset.append(toJson());
+ return asset.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof WebAsset)) {
+ return false;
+ }
+
+ return ((WebAsset) o).toJson().equals(toJson());
+ }
+
+ @Override
+ public int hashCode() {
+ return toJson().hashCode();
+ }
+
+ @Override
+ public int lookupKey() {
+ return toJson().hashCode();
+ }
+
+ /**
+ * Checks that the input is a valid web asset.
+ *
+ * @throws AssociationServiceException if the asset is not well formatted.
+ */
+ protected static WebAsset create(JSONObject asset)
+ throws AssociationServiceException {
+ if (asset.optString(Utils.WEB_ASSET_FIELD_SITE).equals("")) {
+ throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
+ Utils.WEB_ASSET_FIELD_SITE));
+ }
+
+ URL url;
+ try {
+ url = new URL(asset.optString(Utils.WEB_ASSET_FIELD_SITE));
+ } catch (MalformedURLException e) {
+ throw new AssociationServiceException("Url is not well formatted.", e);
+ }
+
+ String scheme = url.getProtocol().toLowerCase(Locale.US);
+ if (!scheme.equals("https") && !scheme.equals("http")) {
+ throw new AssociationServiceException("Expected scheme to be http or https.");
+ }
+
+ if (url.getUserInfo() != null) {
+ throw new AssociationServiceException("The url should not contain user info.");
+ }
+
+ String path = url.getFile(); // This is url.getPath() + url.getQuery().
+ if (!path.equals("/") && !path.equals("")) {
+ throw new AssociationServiceException(
+ "Site should only have scheme, domain, and port.");
+ }
+
+ return new WebAsset(url);
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/WebAssetMatcher.java b/packages/StatementService/src/com/android/statementservice/retriever/WebAssetMatcher.java
new file mode 100644
index 0000000..8a1078b
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/WebAssetMatcher.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+/**
+ * Match assets that have the same 'site' field.
+ */
+/* package private */ final class WebAssetMatcher extends AbstractAssetMatcher {
+
+ private final WebAsset mQuery;
+
+ public WebAssetMatcher(WebAsset query) {
+ mQuery = query;
+ }
+
+ @Override
+ public boolean matches(AbstractAsset asset) {
+ if (asset instanceof WebAsset) {
+ WebAsset webAsset = (WebAsset) asset;
+ return webAsset.toJson().equals(mQuery.toJson());
+ }
+ return false;
+ }
+
+ @Override
+ public int getMatchedLookupKey() {
+ return mQuery.lookupKey();
+ }
+}
diff --git a/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java b/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java
new file mode 100644
index 0000000..86a635c
--- /dev/null
+++ b/packages/StatementService/src/com/android/statementservice/retriever/WebContent.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 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.statementservice.retriever;
+
+/**
+ * An immutable value type representing the response from a web server.
+ *
+ * Visible for testing.
+ *
+ * @hide
+ */
+public final class WebContent {
+
+ private final String mContent;
+ private final Long mExpireTimeMillis;
+
+ public WebContent(String content, Long expireTimeMillis) {
+ mContent = content;
+ mExpireTimeMillis = expireTimeMillis;
+ }
+
+ /**
+ * Returns the expiration time of the content as specified in the HTTP header.
+ */
+ public Long getExpireTimeMillis() {
+ return mExpireTimeMillis;
+ }
+
+ /**
+ * Returns content of the HTTP message body.
+ */
+ public String getContent() {
+ return mContent;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index dd28734..d8e732e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -748,6 +748,11 @@
synchronized (this) {
if (DEBUG) Log.d(TAG, "setKeyguardEnabled(" + enabled + ")");
+ if (isSecure()) {
+ Log.d(TAG, "current mode is SecurityMode, ignore hide keyguard");
+ return;
+ }
+
mExternallyEnabled = enabled;
if (!enabled && mShowing) {
diff --git a/rs/java/android/renderscript/RenderScript.java b/rs/java/android/renderscript/RenderScript.java
index 7ef17a7..fd19d49 100644
--- a/rs/java/android/renderscript/RenderScript.java
+++ b/rs/java/android/renderscript/RenderScript.java
@@ -16,7 +16,6 @@
package android.renderscript;
-import java.io.File;
import java.lang.reflect.Method;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -129,8 +128,6 @@
native void nContextInitToClient(long con);
native void nContextDeinitToClient(long con);
- static File mCacheDir;
-
// this should be a monotonically increasing ID
// used in conjunction with the API version of a device
static final long sMinorID = 1;
@@ -146,23 +143,6 @@
return sMinorID;
}
- /**
- * Sets the directory to use as a persistent storage for the
- * renderscript object file cache.
- *
- * @hide
- * @param cacheDir A directory the current process can write to
- */
- public static void setupDiskCache(File cacheDir) {
- if (!sInitialized) {
- Log.e(LOG_TAG, "RenderScript.setupDiskCache() called when disabled");
- return;
- }
-
- // Defer creation of cache path to nScriptCCreate().
- mCacheDir = cacheDir;
- }
-
/**
* ContextType specifies the specific type of context to be created.
*
diff --git a/rs/java/android/renderscript/RenderScriptCacheDir.java b/rs/java/android/renderscript/RenderScriptCacheDir.java
new file mode 100644
index 0000000..95a9d75
--- /dev/null
+++ b/rs/java/android/renderscript/RenderScriptCacheDir.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2008-2015 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.renderscript;
+
+import java.io.File;
+
+/**
+ * Used only for tracking the RenderScript cache directory.
+ * @hide
+ */
+public class RenderScriptCacheDir {
+ /**
+ * Sets the directory to use as a persistent storage for the
+ * renderscript object file cache.
+ *
+ * @hide
+ * @param cacheDir A directory the current process can write to
+ */
+ public static void setupDiskCache(File cacheDir) {
+ // Defer creation of cache path to nScriptCCreate().
+ mCacheDir = cacheDir;
+ }
+
+ static File mCacheDir;
+
+}
diff --git a/rs/java/android/renderscript/ScriptC.java b/rs/java/android/renderscript/ScriptC.java
index 64d21e4..bf706c1 100644
--- a/rs/java/android/renderscript/ScriptC.java
+++ b/rs/java/android/renderscript/ScriptC.java
@@ -124,7 +124,7 @@
// Create the RS cache path if we haven't done so already.
if (mCachePath == null) {
- File f = new File(rs.mCacheDir, CACHE_PATH);
+ File f = new File(RenderScriptCacheDir.mCacheDir, CACHE_PATH);
mCachePath = f.getAbsolutePath();
f.mkdirs();
}
@@ -135,7 +135,7 @@
private static synchronized long internalStringCreate(RenderScript rs, String resName, byte[] bitcode) {
// Create the RS cache path if we haven't done so already.
if (mCachePath == null) {
- File f = new File(rs.mCacheDir, CACHE_PATH);
+ File f = new File(RenderScriptCacheDir.mCacheDir, CACHE_PATH);
mCachePath = f.getAbsolutePath();
f.mkdirs();
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 18ab3b4..607e09c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -17467,7 +17467,8 @@
fd = ParcelFileDescriptor.open(heapdumpFile,
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_TRUNCATE |
- ParcelFileDescriptor.MODE_READ_WRITE);
+ ParcelFileDescriptor.MODE_WRITE_ONLY |
+ ParcelFileDescriptor.MODE_APPEND);
IApplicationThread thread = myProc.thread;
if (thread != null) {
try {
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index f8e9cbf..d08cddc 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -156,8 +156,7 @@
static final int LOCK_TASK_START_MSG = FIRST_SUPERVISOR_STACK_MSG + 9;
static final int LOCK_TASK_END_MSG = FIRST_SUPERVISOR_STACK_MSG + 10;
static final int CONTAINER_CALLBACK_TASK_LIST_EMPTY = FIRST_SUPERVISOR_STACK_MSG + 11;
- static final int CONTAINER_TASK_LIST_EMPTY_TIMEOUT = FIRST_SUPERVISOR_STACK_MSG + 12;
- static final int LAUNCH_TASK_BEHIND_COMPLETE = FIRST_SUPERVISOR_STACK_MSG + 13;
+ static final int LAUNCH_TASK_BEHIND_COMPLETE = FIRST_SUPERVISOR_STACK_MSG + 12;
private final static String VIRTUAL_DISPLAY_BASE_NAME = "ActivityViewVirtualDisplay";
@@ -3803,15 +3802,6 @@
}
}
} break;
- case CONTAINER_TASK_LIST_EMPTY_TIMEOUT: {
- synchronized (mService) {
- Slog.w(TAG, "Timeout waiting for all activities in task to finish. " +
- msg.obj);
- final ActivityContainer container = (ActivityContainer) msg.obj;
- container.mStack.finishAllActivitiesLocked(true);
- container.onTaskListEmptyLocked();
- }
- } break;
case LAUNCH_TASK_BEHIND_COMPLETE: {
synchronized (mService) {
ActivityRecord r = ActivityRecord.forTokenLocked((IBinder) msg.obj);
@@ -3916,10 +3906,6 @@
}
mContainerState = CONTAINER_STATE_FINISHING;
- final Message msg =
- mHandler.obtainMessage(CONTAINER_TASK_LIST_EMPTY_TIMEOUT, this);
- mHandler.sendMessageDelayed(msg, 2000);
-
long origId = Binder.clearCallingIdentity();
try {
mStack.finishAllActivitiesLocked(false);
@@ -4039,7 +4025,6 @@
}
void onTaskListEmptyLocked() {
- mHandler.removeMessages(CONTAINER_TASK_LIST_EMPTY_TIMEOUT, this);
detachLocked();
deleteActivityContainer(this);
mHandler.obtainMessage(CONTAINER_CALLBACK_TASK_LIST_EMPTY, this).sendToTarget();
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index e7ba582..4fea889 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -11147,13 +11147,13 @@
startIntentFilterVerifications(args.user.getIdentifier(), pkg);
+ // Call with SCAN_NO_DEX, since dexopt has already been made
if (replace) {
- // Call replacePackageLI with SCAN_NO_DEX, since we already made dexopt
replacePackageLI(pkg, parseFlags, scanFlags | SCAN_REPLACING | SCAN_NO_DEX, args.user,
installerPackageName, volumeUuid, res);
} else {
- installNewPackageLI(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,
- args.user, installerPackageName, volumeUuid, res);
+ installNewPackageLI(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES
+ | SCAN_NO_DEX, args.user, installerPackageName, volumeUuid, res);
}
synchronized (mPackages) {
final PackageSetting ps = mSettings.mPackages.get(pkgName);
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index ac1b0f1..1a30cba 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -1403,6 +1403,9 @@
if (WindowManagerService.SHOW_TRANSACTIONS) WindowManagerService.logSurface(w,
"SIZE " + width + "x" + height, null);
mSurfaceControl.setSize(width, height);
+ mSurfaceControl.setMatrix(
+ mDsDx * w.mHScale, mDtDx * w.mVScale,
+ mDsDy * w.mHScale, mDtDy * w.mVScale);
mAnimator.setPendingLayoutChanges(w.getDisplayId(),
WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER);
if ((w.mAttrs.flags & LayoutParams.FLAG_DIM_BEHIND) != 0) {