diff options
64 files changed, 4117 insertions, 338 deletions
diff --git a/api/current.txt b/api/current.txt index 025bb35..e3d0674 100644 --- a/api/current.txt +++ b/api/current.txt @@ -10548,6 +10548,7 @@ package android.hardware { package android.hardware.display { public final class DisplayManager { + method public android.hardware.display.VirtualDisplay createPrivateVirtualDisplay(java.lang.String, int, int, int, android.view.Surface); method public android.view.Display getDisplay(int); method public android.view.Display[] getDisplays(); method public android.view.Display[] getDisplays(java.lang.String); @@ -10562,6 +10563,11 @@ package android.hardware.display { method public abstract void onDisplayRemoved(int); } + public final class VirtualDisplay { + method public android.view.Display getDisplay(); + method public void release(); + } + } package android.hardware.input { @@ -21327,6 +21333,16 @@ package android.renderscript { method public void setInput(android.renderscript.Allocation); } + public final class ScriptIntrinsicHistogram extends android.renderscript.ScriptIntrinsic { + method public static android.renderscript.ScriptIntrinsicHistogram create(android.renderscript.RenderScript, android.renderscript.Element); + method public void forEach(android.renderscript.Allocation); + method public void forEach_dot(android.renderscript.Allocation); + method public android.renderscript.Script.FieldID getFieldID_Input(); + method public android.renderscript.Script.KernelID getKernelID_seperate(); + method public void setDotCoefficients(float, float, float, float); + method public void setOutput(android.renderscript.Allocation); + } + public final class ScriptIntrinsicLUT extends android.renderscript.ScriptIntrinsic { method public static android.renderscript.ScriptIntrinsicLUT create(android.renderscript.RenderScript, android.renderscript.Element); method public void forEach(android.renderscript.Allocation, android.renderscript.Allocation); @@ -25050,6 +25066,7 @@ package android.view { method public deprecated int getWidth(); method public boolean isValid(); field public static final int DEFAULT_DISPLAY = 0; // 0x0 + field public static final int FLAG_PRIVATE = 4; // 0x4 field public static final int FLAG_SECURE = 2; // 0x2 field public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 1; // 0x1 } diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index 0a7a2e7..dcf50cd 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -20,6 +20,7 @@ import android.content.Context; import android.os.Handler; import android.util.SparseArray; import android.view.Display; +import android.view.Surface; import java.util.ArrayList; @@ -134,6 +135,7 @@ public final class DisplayManager { addMatchingDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_WIFI); addMatchingDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_HDMI); addMatchingDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_OVERLAY); + addMatchingDisplaysLocked(mTempDisplays, displayIds, Display.TYPE_VIRTUAL); } return mTempDisplays.toArray(new Display[mTempDisplays.size()]); } finally { @@ -275,6 +277,41 @@ public final class DisplayManager { } /** + * Creates a private virtual display. + * <p> + * The content of a virtual display is rendered to a {@link Surface} provided + * by the application that created the virtual display. + * </p><p> + * Only the application that created a private virtual display is allowed to + * place windows upon it. The private virtual display also does not participate + * in display mirroring: it will neither receive mirrored content from another + * display nor allow its own content to be mirrored elsewhere. More precisely, + * the only processes that are allowed to enumerate or interact with a private + * display are those that have the same UID as the application that originally + * created the private virtual display. + * </p><p> + * The private virtual display should be {@link VirtualDisplay#release released} + * when no longer needed. Because a private virtual display renders to a surface + * provided by the application, it will be released automatically when the + * process terminates and all remaining windows on it will be forcibly removed. + * </p> + * + * @param name The name of the virtual display, must be non-empty. + * @param width The width of the virtual display in pixels, must be greater than 0. + * @param height The height of the virtual display in pixels, must be greater than 0. + * @param densityDpi The density of the virtual display in dpi, must be greater than 0. + * @param surface The surface to which the content of the virtual display should + * be rendered, must be non-null. + * @return The newly created virtual display, or null if the application could + * not create the virtual display. + */ + public VirtualDisplay createPrivateVirtualDisplay(String name, + int width, int height, int densityDpi, Surface surface) { + return mGlobal.createPrivateVirtualDisplay(mContext, + name, width, height, densityDpi, surface); + } + + /** * Listens for changes in available display devices. */ public interface DisplayListener { diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index a858681..3ab882d 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -18,17 +18,20 @@ package android.hardware.display; import android.content.Context; import android.hardware.display.DisplayManager.DisplayListener; +import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.view.CompatibilityInfoHolder; import android.view.Display; import android.view.DisplayInfo; +import android.view.Surface; import java.util.ArrayList; @@ -315,6 +318,53 @@ public final class DisplayManagerGlobal { } } + public VirtualDisplay createPrivateVirtualDisplay(Context context, String name, + int width, int height, int densityDpi, Surface surface) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("name must be non-null and non-empty"); + } + if (width <= 0 || height <= 0 || densityDpi <= 0) { + throw new IllegalArgumentException("width, height, and densityDpi must be " + + "greater than 0"); + } + if (surface == null) { + throw new IllegalArgumentException("surface must not be null"); + } + + Binder token = new Binder(); + int displayId; + try { + displayId = mDm.createPrivateVirtualDisplay(token, context.getPackageName(), + name, width, height, densityDpi, surface); + } catch (RemoteException ex) { + Log.e(TAG, "Could not create private virtual display: " + name, ex); + return null; + } + if (displayId < 0) { + Log.e(TAG, "Could not create private virtual display: " + name); + return null; + } + Display display = getRealDisplay(displayId); + if (display == null) { + Log.wtf(TAG, "Could not obtain display info for newly created " + + "private virtual display: " + name); + try { + mDm.releaseVirtualDisplay(token); + } catch (RemoteException ex) { + } + return null; + } + return new VirtualDisplay(this, display, token); + } + + public void releaseVirtualDisplay(IBinder token) { + try { + mDm.releaseVirtualDisplay(token); + } catch (RemoteException ex) { + Log.w(TAG, "Failed to release virtual display.", ex); + } + } + private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub { @Override public void onDisplayEvent(int displayId, int event) { diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl index 79aad78..cd4896a 100644 --- a/core/java/android/hardware/display/IDisplayManager.aidl +++ b/core/java/android/hardware/display/IDisplayManager.aidl @@ -20,6 +20,7 @@ import android.hardware.display.IDisplayManagerCallback; import android.hardware.display.WifiDisplay; import android.hardware.display.WifiDisplayStatus; import android.view.DisplayInfo; +import android.view.Surface; /** @hide */ interface IDisplayManager { @@ -46,4 +47,11 @@ interface IDisplayManager { // No permissions required. WifiDisplayStatus getWifiDisplayStatus(); + + // No permissions required. + int createPrivateVirtualDisplay(IBinder token, String packageName, + String name, int width, int height, int densityDpi, in Surface surface); + + // No permissions required but must be same Uid as the creator. + void releaseVirtualDisplay(in IBinder token); } diff --git a/core/java/android/hardware/display/VirtualDisplay.java b/core/java/android/hardware/display/VirtualDisplay.java new file mode 100644 index 0000000..145a217 --- /dev/null +++ b/core/java/android/hardware/display/VirtualDisplay.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2013 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.hardware.display; + +import android.os.IBinder; +import android.view.Display; + +/** + * Represents a virtual display. + * + * @see DisplayManager#createPrivateVirtualDisplay + */ +public final class VirtualDisplay { + private final DisplayManagerGlobal mGlobal; + private final Display mDisplay; + private IBinder mToken; + + VirtualDisplay(DisplayManagerGlobal global, Display display, IBinder token) { + mGlobal = global; + mDisplay = display; + mToken = token; + } + + /** + * Gets the virtual display. + */ + public Display getDisplay() { + return mDisplay; + } + + /** + * Releases the virtual display and destroys its underlying surface. + * <p> + * All remaining windows on the virtual display will be forcibly removed + * as part of releasing the virtual display. + * </p> + */ + public void release() { + if (mToken != null) { + mGlobal.releaseVirtualDisplay(mToken); + mToken = null; + } + } + + @Override + public String toString() { + return "VirtualDisplay{display=" + mDisplay + ", token=" + mToken + "}"; + } +} diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 220b997..e59f5b1 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -5105,9 +5105,8 @@ public final class ContactsContract { * Value of 1 implies true, 0 implies false when 0 is the default. * When a cursor is returned to the client, it should check for an extra with the name * {@link ContactsContract#DEFERRED_SNIPPETING} in the cursor. If it exists, the client - * should do its own snippeting using {@link ContactsContract#snippetize}. If - * it doesn't exist, the snippet column in the cursor should already contain a snippetized - * string. + * should do its own snippeting. If it doesn't exist, the snippet column in the cursor + * should already contain a snippetized string. * * @hide */ @@ -8463,138 +8462,4 @@ public final class ContactsContract { public static final String DATA_SET = "com.android.contacts.extra.DATA_SET"; } } - - /** - * Creates a snippet out of the given content that matches the given query. - * @param content - The content to use to compute the snippet. - * @param displayName - Display name for the contact - if this already contains the search - * content, no snippet should be shown. - * @param query - String to search for in the content. - * @param snippetStartMatch - Marks the start of the matching string in the snippet. - * @param snippetEndMatch - Marks the end of the matching string in the snippet. - * @param snippetEllipsis - Ellipsis string appended to the end of the snippet (if too long). - * @param snippetMaxTokens - Maximum number of words from the snippet that will be displayed. - * @return The computed snippet, or null if the snippet could not be computed or should not be - * shown. - * - * @hide - */ - public static String snippetize(String content, String displayName, String query, - char snippetStartMatch, char snippetEndMatch, String snippetEllipsis, - int snippetMaxTokens) { - - String lowerQuery = query != null ? query.toLowerCase() : null; - if (TextUtils.isEmpty(content) || TextUtils.isEmpty(query) || - TextUtils.isEmpty(displayName) || !content.toLowerCase().contains(lowerQuery)) { - return null; - } - - // If the display name already contains the query term, return empty - snippets should - // not be needed in that case. - String lowerDisplayName = displayName != null ? displayName.toLowerCase() : ""; - List<String> nameTokens = new ArrayList<String>(); - List<Integer> nameTokenOffsets = new ArrayList<Integer>(); - split(lowerDisplayName.trim(), nameTokens, nameTokenOffsets); - for (String nameToken : nameTokens) { - if (nameToken.startsWith(lowerQuery)) { - return null; - } - } - - String[] contentLines = content.split("\n"); - - // Locate the lines of the content that contain the query term. - for (String contentLine : contentLines) { - if (contentLine.toLowerCase().contains(lowerQuery)) { - - // Line contains the query string - now search for it at the start of tokens. - List<String> lineTokens = new ArrayList<String>(); - List<Integer> tokenOffsets = new ArrayList<Integer>(); - split(contentLine, lineTokens, tokenOffsets); - - // As we find matches against the query, we'll populate this list with the marked - // (or unchanged) tokens. - List<String> markedTokens = new ArrayList<String>(); - - int firstToken = -1; - int lastToken = -1; - for (int i = 0; i < lineTokens.size(); i++) { - String token = lineTokens.get(i); - String lowerToken = token.toLowerCase(); - if (lowerToken.startsWith(lowerQuery)) { - - // Query term matched; surround the token with match markers. - markedTokens.add(snippetStartMatch + token + snippetEndMatch); - - // If this is the first token found with a match, mark the token - // positions to use for assembling the snippet. - if (firstToken == -1) { - firstToken = - Math.max(0, i - (int) Math.floor( - Math.abs(snippetMaxTokens) - / 2.0)); - lastToken = - Math.min(lineTokens.size(), firstToken + - Math.abs(snippetMaxTokens)); - } - } else { - markedTokens.add(token); - } - } - - // Assemble the snippet by piecing the tokens back together. - if (firstToken > -1) { - StringBuilder sb = new StringBuilder(); - if (firstToken > 0) { - sb.append(snippetEllipsis); - } - for (int i = firstToken; i < lastToken; i++) { - String markedToken = markedTokens.get(i); - String originalToken = lineTokens.get(i); - sb.append(markedToken); - if (i < lastToken - 1) { - // Add the characters that appeared between this token and the next. - sb.append(contentLine.substring( - tokenOffsets.get(i) + originalToken.length(), - tokenOffsets.get(i + 1))); - } - } - if (lastToken < lineTokens.size()) { - sb.append(snippetEllipsis); - } - return sb.toString(); - } - } - } - return null; - } - - /** - * Pattern for splitting a line into tokens. This matches e-mail addresses as a single token, - * otherwise splitting on any group of non-alphanumeric characters. - * - * @hide - */ - private static Pattern SPLIT_PATTERN = - Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+"); - - /** - * Helper method for splitting a string into tokens. The lists passed in are populated with the - * tokens and offsets into the content of each token. The tokenization function parses e-mail - * addresses as a single token; otherwise it splits on any non-alphanumeric character. - * @param content Content to split. - * @param tokens List of token strings to populate. - * @param offsets List of offsets into the content for each token returned. - * - * @hide - */ - private static void split(String content, List<String> tokens, List<Integer> offsets) { - Matcher matcher = SPLIT_PATTERN.matcher(content); - while (matcher.find()) { - tokens.add(matcher.group()); - offsets.add(matcher.start()); - } - } - - } diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index e6a7950..4d984fd 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -20,6 +20,7 @@ import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; +import android.os.Process; import android.os.SystemClock; import android.util.DisplayMetrics; import android.util.Log; @@ -57,6 +58,8 @@ public final class Display { private final int mFlags; private final int mType; private final String mAddress; + private final int mOwnerUid; + private final String mOwnerPackageName; private final CompatibilityInfoHolder mCompatibilityInfo; private DisplayInfo mDisplayInfo; // never null @@ -143,6 +146,18 @@ public final class Display { public static final int FLAG_SECURE = 1 << 1; /** + * Display flag: Indicates that the display is private. Only the application that + * owns the display can create windows on it. + * <p> + * This flag is associated with displays that were created using + * {@link android.hardware.display.DisplayManager#createPrivateVirtualDisplay}. + * </p> + * + * @see #getFlags + */ + public static final int FLAG_PRIVATE = 1 << 2; + + /** * Display type: Unknown display type. * @hide */ @@ -173,6 +188,12 @@ public final class Display { public static final int TYPE_OVERLAY = 4; /** + * Display type: Virtual display. + * @hide + */ + public static final int TYPE_VIRTUAL = 5; + + /** * Internal method to create a display. * Applications should use {@link android.view.WindowManager#getDefaultDisplay()} * or {@link android.hardware.display.DisplayManager#getDisplay} @@ -194,6 +215,8 @@ public final class Display { mFlags = displayInfo.flags; mType = displayInfo.type; mAddress = displayInfo.address; + mOwnerUid = displayInfo.ownerUid; + mOwnerPackageName = displayInfo.ownerPackageName; } /** @@ -262,6 +285,7 @@ public final class Display { * * @see #FLAG_SUPPORTS_PROTECTED_BUFFERS * @see #FLAG_SECURE + * @see #FLAG_PRIVATE */ public int getFlags() { return mFlags; @@ -277,6 +301,7 @@ public final class Display { * @see #TYPE_HDMI * @see #TYPE_WIFI * @see #TYPE_OVERLAY + * @see #TYPE_VIRTUAL * @hide */ public int getType() { @@ -295,6 +320,32 @@ public final class Display { } /** + * Gets the UID of the application that owns this display, or zero if it is + * owned by the system. + * <p> + * If the display is private, then only the owner can use it. + * </p> + * + * @hide + */ + public int getOwnerUid() { + return mOwnerUid; + } + + /** + * Gets the package name of the application that owns this display, or null if it is + * owned by the system. + * <p> + * If the display is private, then only the owner can use it. + * </p> + * + * @hide + */ + public String getOwnerPackageName() { + return mOwnerPackageName; + } + + /** * Gets the compatibility info used by this display instance. * * @return The compatibility info holder, or null if none is required. @@ -564,6 +615,22 @@ public final class Display { } } + /** + * Returns true if the specified UID has access to this display. + * @hide + */ + public boolean hasAccess(int uid) { + return Display.hasAccess(uid, mFlags, mOwnerUid); + } + + /** @hide */ + public static boolean hasAccess(int uid, int flags, int ownerUid) { + return (flags & Display.FLAG_PRIVATE) == 0 + || uid == ownerUid + || uid == Process.SYSTEM_UID + || uid == 0; + } + private void updateDisplayInfoLocked() { // Note: The display manager caches display info objects on our behalf. DisplayInfo newInfo = mGlobal.getDisplayInfo(mDisplayId); @@ -624,6 +691,8 @@ public final class Display { return "WIFI"; case TYPE_OVERLAY: return "OVERLAY"; + case TYPE_VIRTUAL: + return "VIRTUAL"; default: return Integer.toString(type); } diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 9fcd9b1..1442cb7 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -19,6 +19,7 @@ package android.view; import android.content.res.CompatibilityInfo; import android.os.Parcel; import android.os.Parcelable; +import android.os.Process; import android.util.DisplayMetrics; import libcore.util.Objects; @@ -177,6 +178,23 @@ public final class DisplayInfo implements Parcelable { */ public float physicalYDpi; + /** + * The UID of the application that owns this display, or zero if it is owned by the system. + * <p> + * If the display is private, then only the owner can use it. + * </p> + */ + public int ownerUid; + + /** + * The package name of the application that owns this display, or null if it is + * owned by the system. + * <p> + * If the display is private, then only the owner can use it. + * </p> + */ + public String ownerPackageName; + public static final Creator<DisplayInfo> CREATOR = new Creator<DisplayInfo>() { @Override public DisplayInfo createFromParcel(Parcel source) { @@ -228,7 +246,9 @@ public final class DisplayInfo implements Parcelable { && refreshRate == other.refreshRate && logicalDensityDpi == other.logicalDensityDpi && physicalXDpi == other.physicalXDpi - && physicalYDpi == other.physicalYDpi; + && physicalYDpi == other.physicalYDpi + && ownerUid == other.ownerUid + && Objects.equal(ownerPackageName, other.ownerPackageName); } @Override @@ -259,6 +279,8 @@ public final class DisplayInfo implements Parcelable { logicalDensityDpi = other.logicalDensityDpi; physicalXDpi = other.physicalXDpi; physicalYDpi = other.physicalYDpi; + ownerUid = other.ownerUid; + ownerPackageName = other.ownerPackageName; } public void readFromParcel(Parcel source) { @@ -284,6 +306,8 @@ public final class DisplayInfo implements Parcelable { logicalDensityDpi = source.readInt(); physicalXDpi = source.readFloat(); physicalYDpi = source.readFloat(); + ownerUid = source.readInt(); + ownerPackageName = source.readString(); } @Override @@ -310,6 +334,8 @@ public final class DisplayInfo implements Parcelable { dest.writeInt(logicalDensityDpi); dest.writeFloat(physicalXDpi); dest.writeFloat(physicalYDpi); + dest.writeInt(ownerUid); + dest.writeString(ownerPackageName); } @Override @@ -335,6 +361,13 @@ public final class DisplayInfo implements Parcelable { logicalHeight : logicalWidth; } + /** + * Returns true if the specified UID has access to this display. + */ + public boolean hasAccess(int uid) { + return Display.hasAccess(uid, flags, ownerUid); + } + private void getMetricsWithSize(DisplayMetrics outMetrics, CompatibilityInfoHolder cih, int width, int height) { outMetrics.densityDpi = outMetrics.noncompatDensityDpi = logicalDensityDpi; @@ -402,8 +435,13 @@ public final class DisplayInfo implements Parcelable { sb.append(layerStack); sb.append(", type "); sb.append(Display.typeToString(type)); - sb.append(", address "); - sb.append(address); + if (address != null) { + sb.append(", address ").append(address); + } + if (ownerUid != 0 || ownerPackageName != null) { + sb.append(", owner ").append(ownerPackageName); + sb.append(" (uid ").append(ownerUid).append(")"); + } sb.append(flagsToString(flags)); sb.append("}"); return sb.toString(); @@ -417,6 +455,9 @@ public final class DisplayInfo implements Parcelable { if ((flags & Display.FLAG_SUPPORTS_PROTECTED_BUFFERS) != 0) { result.append(", FLAG_SUPPORTS_PROTECTED_BUFFERS"); } + if ((flags & Display.FLAG_PRIVATE) != 0) { + result.append(", FLAG_PRIVATE"); + } return result.toString(); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 109b8e0..e62abbe 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6442,7 +6442,10 @@ public final class ViewRootImpl implements ViewParent, public void runOrPost(View source) { if (mSource != null) { - mSource = getCommonPredecessor(mSource, source); + // If there is no common predecessor, then mSource points to + // a removed view, hence in this case always prefer the source. + View predecessor = getCommonPredecessor(mSource, source); + mSource = (predecessor != null) ? predecessor : source; return; } mSource = source; diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java index a324502..3afab09 100644 --- a/core/java/android/webkit/WebViewClassic.java +++ b/core/java/android/webkit/WebViewClassic.java @@ -66,6 +66,7 @@ import android.text.Editable; import android.text.InputType; import android.text.Selection; import android.text.TextUtils; +import android.util.AndroidRuntimeException; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; @@ -1293,6 +1294,19 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // WebViewProvider bindings static class Factory implements WebViewFactoryProvider, WebViewFactoryProvider.Statics { + Factory() { + // Touch JniUtil and WebViewCore in case this is being called from + // WebViewFactory.Preloader, to ensure that the JNI libraries that they use are + // preloaded in the zygote. + try { + Class.forName("android.webkit.JniUtil"); + Class.forName("android.webkit.WebViewCore"); + } catch (ClassNotFoundException e) { + Log.e(LOGTAG, "failed to load JNI libraries"); + throw new AndroidRuntimeException(e); + } + } + @Override public String findAddress(String addr) { return WebViewClassic.findAddress(addr); diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index ea5187a..2ee0961 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -19,10 +19,9 @@ package android.webkit; import android.os.Build; import android.os.StrictMode; import android.os.SystemProperties; +import android.util.AndroidRuntimeException; import android.util.Log; -import dalvik.system.PathClassLoader; - /** * Top level factory, used creating all the main WebView implementation classes. * @@ -45,6 +44,17 @@ public final class WebViewFactory { private static final boolean DEBUG = false; + private static class Preloader { + static WebViewFactoryProvider sPreloadedProvider; + static { + try { + sPreloadedProvider = getFactoryClass().newInstance(); + } catch (Exception e) { + Log.w(LOGTAG, "error preloading provider", e); + } + } + } + // Cache the factory both for efficiency, and ensure any one process gets all webviews from the // same provider. private static WebViewFactoryProvider sProviderInstance; @@ -67,32 +77,39 @@ public final class WebViewFactory { // us honest and minimize usage of WebViewClassic internals when binding the proxy. if (sProviderInstance != null) return sProviderInstance; - if (isExperimentalWebViewEnabled()) { - StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); - try { - sProviderInstance = getFactoryByName(CHROMIUM_WEBVIEW_FACTORY); - if (DEBUG) Log.v(LOGTAG, "Loaded Chromium provider: " + sProviderInstance); - } finally { - StrictMode.setThreadPolicy(oldPolicy); - } + Class<WebViewFactoryProvider> providerClass; + try { + providerClass = getFactoryClass(); + } catch (ClassNotFoundException e) { + Log.e(LOGTAG, "error loading provider", e); + throw new AndroidRuntimeException(e); } - if (sProviderInstance == null) { - if (DEBUG) Log.v(LOGTAG, "Falling back to default provider: " - + DEFAULT_WEBVIEW_FACTORY); - sProviderInstance = getFactoryByName(DEFAULT_WEBVIEW_FACTORY); - if (sProviderInstance == null) { - if (DEBUG) Log.v(LOGTAG, "Falling back to explicit linkage"); - sProviderInstance = new WebViewClassic.Factory(); - } + // This implicitly loads Preloader even if it wasn't preloaded at boot. + if (Preloader.sPreloadedProvider != null && + Preloader.sPreloadedProvider.getClass() == providerClass) { + sProviderInstance = Preloader.sPreloadedProvider; + if (DEBUG) Log.v(LOGTAG, "Using preloaded provider: " + sProviderInstance); + return sProviderInstance; + } + + // The preloaded provider isn't the one we wanted; construct our own. + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); + try { + sProviderInstance = providerClass.newInstance(); + if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance); + return sProviderInstance; + } catch (Exception e) { + Log.e(LOGTAG, "error instantiating provider", e); + throw new AndroidRuntimeException(e); + } finally { + StrictMode.setThreadPolicy(oldPolicy); } - return sProviderInstance; } } - // For debug builds, we allow a system property to specify that we should use the - // experimtanl Chromium powered WebView. This enables us to switch between - // implementations at runtime. For user (release) builds, don't allow this. + // We allow a system property to specify that we should use the experimental Chromium powered + // WebView. This enables us to switch between implementations at runtime. private static boolean isExperimentalWebViewEnabled() { if (!isExperimentalWebViewAvailable()) return false; boolean use_experimental_webview = SystemProperties.getBoolean( @@ -108,19 +125,11 @@ public final class WebViewFactory { return use_experimental_webview; } - private static WebViewFactoryProvider getFactoryByName(String providerName) { - try { - if (DEBUG) Log.v(LOGTAG, "attempt to load class " + providerName); - Class<?> c = Class.forName(providerName); - if (DEBUG) Log.v(LOGTAG, "instantiating factory"); - return (WebViewFactoryProvider) c.newInstance(); - } catch (ClassNotFoundException e) { - Log.e(LOGTAG, "error loading " + providerName, e); - } catch (IllegalAccessException e) { - Log.e(LOGTAG, "error loading " + providerName, e); - } catch (InstantiationException e) { - Log.e(LOGTAG, "error loading " + providerName, e); + private static Class<WebViewFactoryProvider> getFactoryClass() throws ClassNotFoundException { + if (isExperimentalWebViewEnabled()) { + return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY); + } else { + return (Class<WebViewFactoryProvider>) Class.forName(DEFAULT_WEBVIEW_FACTORY); } - return null; } } diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index f0d80e6..d114b76 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -1457,14 +1457,22 @@ public class HorizontalScrollView extends FrameLayout { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { - // There is only one child - final View child = getChildAt(0); - final int childWidth = child.getMeasuredWidth(); - final LayoutParams childParams = (LayoutParams) child.getLayoutParams(); + int childWidth = 0; + int childMargins = 0; + + if (getChildCount() > 0) { + childWidth = getChildAt(0).getMeasuredWidth(); + LayoutParams childParams = (LayoutParams) getChildAt(0).getLayoutParams(); + childMargins = childParams.leftMargin + childParams.rightMargin; + } + final int available = r - l - getPaddingLeftWithForeground() - - getPaddingRightWithForeground() - childParams.leftMargin - childParams.rightMargin; + getPaddingRightWithForeground() - childMargins; + final boolean forceLeftGravity = (childWidth > available); + layoutChildren(l, t, r, b, forceLeftGravity); + mIsLayoutDirty = false; // Give a child focus if it needs it if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index b32cfbc..3d361f1 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -1473,16 +1473,16 @@ public class ScrollView extends FrameLayout { } mChildToScrollTo = null; - // There is only one child - final View child = getChildAt(0); - final int childHeight = child.getMeasuredHeight(); if (!hasLayout()) { - final int scrollRange = Math.max(0, - childHeight - (b - t - mPaddingBottom - mPaddingTop)); if (mSavedState != null) { mScrollY = mSavedState.scrollPosition; mSavedState = null; } // mScrollY default value is "0" + + final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; + final int scrollRange = Math.max(0, + childHeight - (b - t - mPaddingBottom - mPaddingTop)); + // Don't forget to clamp if (mScrollY > scrollRange) { mScrollY = scrollRange; diff --git a/docs/html/guide/appendix/media-formats.jd b/docs/html/guide/appendix/media-formats.jd index 069a603..9070968 100644 --- a/docs/html/guide/appendix/media-formats.jd +++ b/docs/html/guide/appendix/media-formats.jd @@ -259,7 +259,7 @@ rates for raw PCM recordings at 8000, 16000 and 44100 Hz.</td> <tr> <td>VP8</td> -<td> </td> +<td style="text-align: center;" nowrap><big>•</big><br><small>(Android 4.3+)</small></td> <td style="text-align: center;" nowrap><big>•</big><br><small>(Android 2.3.3+)</small></td> <td>Streamable only in Android 4.0 and above</td> <td> @@ -272,9 +272,17 @@ rates for raw PCM recordings at 8000, 16000 and 44100 Hz.</td> <h2 id="recommendations">Video Encoding Recommendations</h2> -<p>Table 2, below, lists examples of video encoding profiles and parameters that the Android media framework supports for playback. In addition to these encoding parameter recommendations, a device's available <em>video recording</em> profiles can be used as a proxy for media playback capabilities. These profiles can be inspected using the {@link android.media.CamcorderProfile CamcorderProfile} class, which is available since API level 8.</p> +<p>Table 2, below, lists examples of video encoding profiles and parameters that the Android +media framework supports for playback in the H.264 Baseline Profile codec. While +table 3 lists examples that the framework supports for playback in the VP8 media codec.</p> + +<p>In addition to these encoding parameter recommendations, +a device's available <em>video recording</em> profiles can be used as a proxy for media playback +capabilities. These profiles can be inspected using the {@link android.media.CamcorderProfile +CamcorderProfile} class, which is available since API level 8.</p> -<p class="table-caption" id="encoding-recommendations-table"><strong>Table 2.</strong> Examples of supported video encoding parameters.</p> +<p class="table-caption" id="encoding-recommendations-table"><strong>Table 2.</strong> +Examples of supported video encoding parameters for the H.264 Baseline Profile codec.</p> <table> <thead> @@ -282,17 +290,11 @@ rates for raw PCM recordings at 8000, 16000 and 44100 Hz.</td> <th> </th> <th><acronym title="Standard definition">SD</a> (Low quality)</th> <th><acronym title="Standard definition">SD</a> (High quality)</th> - <th><acronym title="High definition">HD</a> (Not available on all devices)</th> + <th><acronym title="High definition">HD 720p</a> (N/A on all devices)</th> </tr> </thead> <tbody> <tr> - <th>Video codec</th> - <td>H.264 Baseline Profile</td> - <td>H.264 Baseline Profile</td> - <td>H.264 Baseline Profile</td> - </tr> - <tr> <th>Video resolution</th> <td>176 x 144 px</td> <td>480 x 360 px</td> @@ -331,6 +333,49 @@ rates for raw PCM recordings at 8000, 16000 and 44100 Hz.</td> </tbody> </table> + + +<p class="table-caption" id="encoding-recommendations-table-vp8"><strong>Table 3.</strong> +Examples of supported video encoding parameters for the VP8 codec.</p> + +<table> + <thead> + <tr> + <th> </th> + <th><acronym title="Standard definition">SD</a> (Low quality)</th> + <th><acronym title="Standard definition">SD</a> (High quality)</th> + <th><acronym title="High definition">HD 720p</a> (N/A on all devices)</th> + <th><acronym title="High definition">HD 1080p</a> (N/A on all devices)</th> + </tr> + </thead> + <tbody> + <tr> + <th>Video resolution</th> + <td>320 x 180 px</td> + <td>640 x 360 px</td> + <td>1280 x 720 px</td> + <td>1920 x 1080 px</td> + </tr> + <tr> + <th>Video frame rate</th> + <td>30 fps</td> + <td>30 fps</td> + <td>30 fps</td> + <td>30 fps</td> + </tr> + <tr> + <th>Video bitrate</th> + <td>800 Kbps</td> + <td>2 Mbps</td> + <td>4 Mbps</td> + <td>10 Mbps</td> + </tr> + </tbody> +</table> + + + + <p style="margin-top: 2em">For video content that is streamed over HTTP or RTSP, there are additional requirements:</p> <ul> diff --git a/graphics/java/android/renderscript/ScriptIntrinsicConvolve3x3.java b/graphics/java/android/renderscript/ScriptIntrinsicConvolve3x3.java index c9c54b2..d54b675 100644 --- a/graphics/java/android/renderscript/ScriptIntrinsicConvolve3x3.java +++ b/graphics/java/android/renderscript/ScriptIntrinsicConvolve3x3.java @@ -48,7 +48,10 @@ public final class ScriptIntrinsicConvolve3x3 extends ScriptIntrinsic { */ public static ScriptIntrinsicConvolve3x3 create(RenderScript rs, Element e) { float f[] = { 0, 0, 0, 0, 1, 0, 0, 0, 0}; - if (!e.isCompatible(Element.U8_4(rs))) { + if (!e.isCompatible(Element.U8(rs)) && + !e.isCompatible(Element.U8_4(rs)) && + !e.isCompatible(Element.F32(rs)) && + !e.isCompatible(Element.F32_4(rs))) { throw new RSIllegalArgumentException("Unsuported element type."); } int id = rs.nScriptIntrinsicCreate(1, e.getID(rs)); diff --git a/graphics/java/android/renderscript/ScriptIntrinsicConvolve5x5.java b/graphics/java/android/renderscript/ScriptIntrinsicConvolve5x5.java index c6e1e39..da4d5a2 100644 --- a/graphics/java/android/renderscript/ScriptIntrinsicConvolve5x5.java +++ b/graphics/java/android/renderscript/ScriptIntrinsicConvolve5x5.java @@ -48,6 +48,13 @@ public final class ScriptIntrinsicConvolve5x5 extends ScriptIntrinsic { * @return ScriptIntrinsicConvolve5x5 */ public static ScriptIntrinsicConvolve5x5 create(RenderScript rs, Element e) { + if (!e.isCompatible(Element.U8(rs)) && + !e.isCompatible(Element.U8_4(rs)) && + !e.isCompatible(Element.F32(rs)) && + !e.isCompatible(Element.F32_4(rs))) { + throw new RSIllegalArgumentException("Unsuported element type."); + } + int id = rs.nScriptIntrinsicCreate(4, e.getID(rs)); return new ScriptIntrinsicConvolve5x5(id, rs); diff --git a/graphics/java/android/renderscript/ScriptIntrinsicHistogram.java b/graphics/java/android/renderscript/ScriptIntrinsicHistogram.java new file mode 100644 index 0000000..f143326 --- /dev/null +++ b/graphics/java/android/renderscript/ScriptIntrinsicHistogram.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2013 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 android.content.Context; +import android.content.res.Resources; +import android.util.Log; + +/** + * Intrinsic Histogram filter. + * + * + **/ +public final class ScriptIntrinsicHistogram extends ScriptIntrinsic { + private Allocation mOut; + + private ScriptIntrinsicHistogram(int id, RenderScript rs) { + super(id, rs); + } + + /** + * Create an intrinsic for calculating the histogram of an uchar + * or uchar4 image. + * + * Supported elements types are + * {@link Element#U8_4}, {@link Element#U8_3}, + * {@link Element#U8_2}, {@link Element#U8} + * + * @param rs The RenderScript context + * @param e Element type for inputs + * + * @return ScriptIntrinsicHistogram + */ + public static ScriptIntrinsicHistogram create(RenderScript rs, Element e) { + if ((!e.isCompatible(Element.U8_4(rs))) && + (!e.isCompatible(Element.U8_3(rs))) && + (!e.isCompatible(Element.U8_2(rs))) && + (!e.isCompatible(Element.U8(rs)))) { + throw new RSIllegalArgumentException("Unsuported element type."); + } + int id = rs.nScriptIntrinsicCreate(9, e.getID(rs)); + ScriptIntrinsicHistogram sib = new ScriptIntrinsicHistogram(id, rs); + return sib; + } + + /** + * Process an input buffer and place the histogram into the + * output allocation. The output allocation may be a narrower + * vector size than the input. In this case the vector size of + * the output is used to determine how many of the input + * channels are used in the computation. This is useful if you + * have an RGBA input buffer but only want the histogram for + * RGB. + * + * 1D and 2D input allocations are supported. + * + * @param ain The input image + */ + public void forEach(Allocation ain) { + if (ain.getType().getElement().getVectorSize() < + mOut.getType().getElement().getVectorSize()) { + + throw new RSIllegalArgumentException( + "Input vector size must be >= output vector size."); + } + if (ain.getType().getElement().isCompatible(Element.U8(mRS)) && + ain.getType().getElement().isCompatible(Element.U8_4(mRS))) { + throw new RSIllegalArgumentException("Output type must be U32 or I32."); + } + + forEach(0, ain, null, null); + } + + /** + * Set the coefficients used for the RGBA to Luminocity + * calculation. The default is {0.299f, 0.587f, 0.114f, 0.f}. + * + * Coefficients must be >= 0 and sum to 1.0 or less. + * + * @param r Red coefficient + * @param g Green coefficient + * @param b Blue coefficient + * @param a Alpha coefficient + */ + public void setDotCoefficients(float r, float g, float b, float a) { + if ((r < 0.f) || (g < 0.f) || (b < 0.f) || (a < 0.f)) { + throw new RSIllegalArgumentException("Coefficient may not be negative."); + } + if ((r + g + b + a) > 1.f) { + throw new RSIllegalArgumentException("Sum of coefficients must be 1.0 or less."); + } + + FieldPacker fp = new FieldPacker(16); + fp.addF32(r); + fp.addF32(g); + fp.addF32(b); + fp.addF32(a); + setVar(0, fp); + } + + /** + * Set the output of the histogram. 32 bit integer types are + * supported. + * + * @param aout The output allocation + */ + public void setOutput(Allocation aout) { + mOut = aout; + if (mOut.getType().getElement() != Element.U32(mRS) && + mOut.getType().getElement() != Element.U32_2(mRS) && + mOut.getType().getElement() != Element.U32_3(mRS) && + mOut.getType().getElement() != Element.U32_4(mRS) && + mOut.getType().getElement() != Element.I32(mRS) && + mOut.getType().getElement() != Element.I32_2(mRS) && + mOut.getType().getElement() != Element.I32_3(mRS) && + mOut.getType().getElement() != Element.I32_4(mRS)) { + + throw new RSIllegalArgumentException("Output type must be U32 or I32."); + } + if ((mOut.getType().getX() != 256) || + (mOut.getType().getY() != 0) || + mOut.getType().hasMipmaps() || + (mOut.getType().getYuv() != 0)) { + + throw new RSIllegalArgumentException("Output must be 1D, 256 elements."); + } + setVar(1, aout); + } + + /** + * Process an input buffer and place the histogram into the + * output allocation. The dot product of the input channel and + * the coefficients from 'setDotCoefficients' are used to + * calculate the output values. + * + * 1D and 2D input allocations are supported. + * + * @param ain The input image + */ + public void forEach_dot(Allocation ain) { + if (mOut.getType().getElement().getVectorSize() != 1) { + throw new RSIllegalArgumentException("Output vector size must be one."); + } + if (ain.getType().getElement().isCompatible(Element.U8(mRS)) && + ain.getType().getElement().isCompatible(Element.U8_4(mRS))) { + throw new RSIllegalArgumentException("Output type must be U32 or I32."); + } + + forEach(1, ain, null, null); + } + + + + /** + * Get a KernelID for this intrinsic kernel. + * + * @return Script.KernelID The KernelID object. + */ + public Script.KernelID getKernelID_seperate() { + return createKernelID(0, 3, null, null); + } + + /** + * Get a FieldID for the input field of this intrinsic. + * + * @return Script.FieldID The FieldID object. + */ + public Script.FieldID getFieldID_Input() { + return createFieldID(1, null); + } +} + diff --git a/packages/Keyguard/AndroidManifest.xml b/packages/Keyguard/AndroidManifest.xml index 7a40a9e..7d77c48 100644 --- a/packages/Keyguard/AndroidManifest.xml +++ b/packages/Keyguard/AndroidManifest.xml @@ -39,7 +39,7 @@ <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" /> <application android:label="@string/app_name" - android:process="com.android.systemui.keyguard" + android:process="com.android.systemui" android:persistent="true" > <service android:name=".KeyguardService" diff --git a/preloaded-classes b/preloaded-classes index 2aa610a..45d27ee 100644 --- a/preloaded-classes +++ b/preloaded-classes @@ -1220,7 +1220,6 @@ android.webkit.HTML5Audio android.webkit.HTML5VideoViewProxy android.webkit.JWebCoreJavaBridge android.webkit.JavascriptInterface -android.webkit.JniUtil android.webkit.L10nUtils android.webkit.MockGeolocation android.webkit.OverScrollGlow @@ -1269,7 +1268,6 @@ android.webkit.WebViewClassic$TitleBarDelegate android.webkit.WebViewClassic$TrustStorageListener android.webkit.WebViewClassic$ViewSizeData android.webkit.WebViewClient -android.webkit.WebViewCore android.webkit.WebViewCore$AutoFillData android.webkit.WebViewCore$DrawData android.webkit.WebViewCore$EventHub @@ -1283,6 +1281,7 @@ android.webkit.WebViewDatabase android.webkit.WebViewDatabaseClassic android.webkit.WebViewDatabaseClassic$1 android.webkit.WebViewFactory +android.webkit.WebViewFactory$Preloader android.webkit.WebViewFactoryProvider android.webkit.WebViewFactoryProvider$Statics android.webkit.WebViewInputDispatcher diff --git a/services/java/com/android/server/am/ActiveServices.java b/services/java/com/android/server/am/ActiveServices.java index c558fbd..cd718a2 100644 --- a/services/java/com/android/server/am/ActiveServices.java +++ b/services/java/com/android/server/am/ActiveServices.java @@ -1511,6 +1511,7 @@ public final class ActiveServices { mPendingServices.remove(i); i--; + proc.addPackage(sr.appInfo.packageName, mAm.mProcessTracker); realStartServiceLocked(sr, proc); didSomething = true; } diff --git a/services/java/com/android/server/am/ActivityManagerService.java b/services/java/com/android/server/am/ActivityManagerService.java index 62520d5..bdfa447 100644 --- a/services/java/com/android/server/am/ActivityManagerService.java +++ b/services/java/com/android/server/am/ActivityManagerService.java @@ -198,32 +198,32 @@ public final class ActivityManagerService extends ActivityManagerNative static final String TAG_MU = "ActivityManagerServiceMU"; static final boolean DEBUG = false; static final boolean localLOGV = DEBUG; - static final boolean DEBUG_SWITCH = localLOGV || false; - static final boolean DEBUG_TASKS = localLOGV || false; - static final boolean DEBUG_THUMBNAILS = localLOGV || false; - static final boolean DEBUG_PAUSE = localLOGV || false; - static final boolean DEBUG_OOM_ADJ = localLOGV || false; - static final boolean DEBUG_TRANSITION = localLOGV || false; + static final boolean DEBUG_BACKUP = localLOGV || false; static final boolean DEBUG_BROADCAST = localLOGV || false; - static final boolean DEBUG_BACKGROUND_BROADCAST = DEBUG_BROADCAST || false; static final boolean DEBUG_BROADCAST_LIGHT = DEBUG_BROADCAST || false; - static final boolean DEBUG_SERVICE = localLOGV || false; - static final boolean DEBUG_SERVICE_EXECUTING = localLOGV || false; - static final boolean DEBUG_VISBILITY = localLOGV || false; - static final boolean DEBUG_PROCESSES = localLOGV || false; - static final boolean DEBUG_PROCESS_OBSERVERS = localLOGV || false; + static final boolean DEBUG_BACKGROUND_BROADCAST = DEBUG_BROADCAST || false; static final boolean DEBUG_CLEANUP = localLOGV || false; - static final boolean DEBUG_PROVIDER = localLOGV || false; - static final boolean DEBUG_URI_PERMISSION = localLOGV || false; - static final boolean DEBUG_USER_LEAVING = localLOGV || false; - static final boolean DEBUG_RESULTS = localLOGV || false; - static final boolean DEBUG_BACKUP = localLOGV || false; static final boolean DEBUG_CONFIGURATION = localLOGV || false; + static final boolean DEBUG_IMMERSIVE = localLOGV || false; + static final boolean DEBUG_MU = localLOGV || false; + static final boolean DEBUG_OOM_ADJ = localLOGV || false; + static final boolean DEBUG_PAUSE = localLOGV || false; static final boolean DEBUG_POWER = localLOGV || false; static final boolean DEBUG_POWER_QUICK = DEBUG_POWER || false; - static final boolean DEBUG_MU = localLOGV || false; - static final boolean DEBUG_IMMERSIVE = localLOGV || false; - static final boolean DEBUG_STACK = localLOGV || true; + static final boolean DEBUG_PROCESS_OBSERVERS = localLOGV || false; + static final boolean DEBUG_PROCESSES = localLOGV || false; + static final boolean DEBUG_PROVIDER = localLOGV || false; + static final boolean DEBUG_RESULTS = localLOGV || false; + static final boolean DEBUG_SERVICE = localLOGV || false; + static final boolean DEBUG_SERVICE_EXECUTING = localLOGV || false; + static final boolean DEBUG_STACK = localLOGV || false; + static final boolean DEBUG_SWITCH = localLOGV || false; + static final boolean DEBUG_TASKS = localLOGV || false; + static final boolean DEBUG_THUMBNAILS = localLOGV || false; + static final boolean DEBUG_TRANSITION = localLOGV || false; + static final boolean DEBUG_URI_PERMISSION = localLOGV || false; + static final boolean DEBUG_USER_LEAVING = localLOGV || false; + static final boolean DEBUG_VISBILITY = localLOGV || false; static final boolean VALIDATE_TOKENS = true; static final boolean SHOW_ACTIVITY_START_TIME = true; @@ -6281,6 +6281,7 @@ public final class ActivityManagerService extends ActivityManagerNative enforceCallingPermission(android.Manifest.permission.REORDER_TASKS, "moveTaskToFront()"); + if (DEBUG_STACK) Slog.d(TAG, "moveTaskToFront: moving task=" + task); synchronized(this) { if (!checkAppSwitchAllowedLocked(Binder.getCallingPid(), Binder.getCallingUid(), "Task to front")) { @@ -6305,6 +6306,7 @@ public final class ActivityManagerService extends ActivityManagerNative synchronized(this) { TaskRecord tr = recentTaskForIdLocked(taskId); if (tr != null) { + if (DEBUG_STACK) Slog.d(TAG, "moveTaskToBack: moving task=" + tr); ActivityStack stack = tr.stack; if (stack.mResumedActivity != null && stack.mResumedActivity.task == tr) { if (!checkAppSwitchAllowedLocked(Binder.getCallingPid(), @@ -6383,6 +6385,8 @@ public final class ActivityManagerService extends ActivityManagerNative new RuntimeException("here").fillInStackTrace()); } synchronized (this) { + if (DEBUG_STACK) Slog.d(TAG, "moveTaskToStack: moving task=" + taskId + " to stackId=" + + stackId + " toTop=" + toTop); mStackSupervisor.moveTaskToStack(taskId, stackId, toTop); } } diff --git a/services/java/com/android/server/am/ActivityStackSupervisor.java b/services/java/com/android/server/am/ActivityStackSupervisor.java index 8224c88..a4fd7ad 100644 --- a/services/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/java/com/android/server/am/ActivityStackSupervisor.java @@ -1186,7 +1186,8 @@ public final class ActivityStackSupervisor { } ActivityStack getCorrectStack(ActivityRecord r) { - if (r.isApplicationActivity() || (r.task != null && r.task.isApplicationTask())) { + final TaskRecord task = r.task; + if (r.isApplicationActivity() || (task != null && task.isApplicationTask())) { int stackNdx; for (stackNdx = mStacks.size() - 1; stackNdx > 0; --stackNdx) { if (mStacks.get(stackNdx).mCurrentUser == mCurrentUser) { @@ -1199,6 +1200,9 @@ public final class ActivityStackSupervisor { StackBox.TASK_STACK_GOES_OVER, 1.0f); mFocusedStack = getStack(stackId); } + if (task != null) { + mFocusedStack = task.stack; + } return mFocusedStack; } return mHomeStack; @@ -1649,6 +1653,7 @@ public final class ActivityStackSupervisor { ActivityStack.logStartActivity(EventLogTags.AM_CREATE_ACTIVITY, r, r.task); setLaunchHomeTaskNextFlag(sourceRecord, r, targetStack); targetStack.startActivityLocked(r, newTask, doResume, keepCurTransition, options); + mService.setFocusedActivityLocked(r); return ActivityManager.START_SUCCESS; } @@ -1867,6 +1872,8 @@ public final class ActivityStackSupervisor { void findTaskToMoveToFrontLocked(int taskId, int flags, Bundle options) { for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) { if (mStacks.get(stackNdx).findTaskToMoveToFrontLocked(taskId, flags, options)) { + if (DEBUG_STACK) Slog.d(TAG, "findTaskToMoveToFront: moved to front of stack=" + + mStacks.get(stackNdx)); return; } } @@ -2053,7 +2060,6 @@ public final class ActivityStackSupervisor { final ActivityStack stack = r.task.stack; if (isFrontStack(stack)) { mService.updateUsageStats(r, true); - mService.setFocusedActivityLocked(r); } if (allResumedActivitiesComplete()) { ensureActivitiesVisibleLocked(null, 0); diff --git a/services/java/com/android/server/display/DisplayDeviceInfo.java b/services/java/com/android/server/display/DisplayDeviceInfo.java index 247d8a0..11f8d6a 100644 --- a/services/java/com/android/server/display/DisplayDeviceInfo.java +++ b/services/java/com/android/server/display/DisplayDeviceInfo.java @@ -61,6 +61,18 @@ final class DisplayDeviceInfo { public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 1 << 3; /** + * Flag: Indicates that the display device is owned by a particular application + * and that no other application should be able to interact with it. + */ + public static final int FLAG_PRIVATE = 1 << 4; + + /** + * Flag: Indicates that the display device is not blanked automatically by + * the power manager. + */ + public static final int FLAG_NEVER_BLANK = 1 << 5; + + /** * Touch attachment: Display does not receive touch. */ public static final int TOUCH_NONE = 0; @@ -150,6 +162,23 @@ final class DisplayDeviceInfo { */ public String address; + /** + * The UID of the application that owns this display, or zero if it is owned by the system. + * <p> + * If the display is private, then only the owner can use it. + * </p> + */ + public int ownerUid; + + /** + * The package name of the application that owns this display, or null if it is + * owned by the system. + * <p> + * If the display is private, then only the owner can use it. + * </p> + */ + public String ownerPackageName; + public void setAssumedDensityForExternalDisplay(int width, int height) { densityDpi = Math.min(width, height) * DisplayMetrics.DENSITY_XHIGH / 1080; // Technically, these values should be smaller than the apparent density @@ -176,7 +205,9 @@ final class DisplayDeviceInfo { && touch == other.touch && rotation == other.rotation && type == other.type - && Objects.equal(address, other.address); + && Objects.equal(address, other.address) + && ownerUid == other.ownerUid + && Objects.equal(ownerPackageName, other.ownerPackageName); } @Override @@ -197,19 +228,32 @@ final class DisplayDeviceInfo { rotation = other.rotation; type = other.type; address = other.address; + ownerUid = other.ownerUid; + ownerPackageName = other.ownerPackageName; } // For debugging purposes @Override public String toString() { - return "DisplayDeviceInfo{\"" + name + "\": " + width + " x " + height + ", " - + refreshRate + " fps, " - + "density " + densityDpi + ", " + xDpi + " x " + yDpi + " dpi" - + ", touch " + touchToString(touch) + flagsToString(flags) - + ", rotation " + rotation - + ", type " + Display.typeToString(type) - + ", address " + address - + "}"; + StringBuilder sb = new StringBuilder(); + sb.append("DisplayDeviceInfo{\""); + sb.append(name).append("\": ").append(width).append(" x ").append(height); + sb.append(", ").append(refreshRate).append(" fps, "); + sb.append("density ").append(densityDpi); + sb.append(", ").append(xDpi).append(" x ").append(yDpi).append(" dpi"); + sb.append(", touch ").append(touchToString(touch)); + sb.append(", rotation ").append(rotation); + sb.append(", type ").append(Display.typeToString(type)); + if (address != null) { + sb.append(", address ").append(address); + } + if (ownerUid != 0 || ownerPackageName != null) { + sb.append(", owner ").append(ownerPackageName); + sb.append(" (uid ").append(ownerUid).append(")"); + } + sb.append(flagsToString(flags)); + sb.append("}"); + return sb.toString(); } private static String touchToString(int touch) { @@ -239,6 +283,12 @@ final class DisplayDeviceInfo { if ((flags & FLAG_SUPPORTS_PROTECTED_BUFFERS) != 0) { msg.append(", FLAG_SUPPORTS_PROTECTED_BUFFERS"); } + if ((flags & FLAG_PRIVATE) != 0) { + msg.append(", FLAG_PRIVATE"); + } + if ((flags & FLAG_NEVER_BLANK) != 0) { + msg.append(", FLAG_NEVER_BLANK"); + } return msg.toString(); } } diff --git a/services/java/com/android/server/display/DisplayManagerService.java b/services/java/com/android/server/display/DisplayManagerService.java index ca85e42..c339c26 100644 --- a/services/java/com/android/server/display/DisplayManagerService.java +++ b/services/java/com/android/server/display/DisplayManagerService.java @@ -33,15 +33,19 @@ import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; +import android.text.TextUtils; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.DisplayInfo; +import android.view.Surface; + import com.android.server.UiThread; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -171,6 +175,9 @@ public final class DisplayManagerService extends IDisplayManager.Stub { // The Wifi display adapter, or null if not registered. private WifiDisplayAdapter mWifiDisplayAdapter; + // The virtual display adapter, or null if not registered. + private VirtualDisplayAdapter mVirtualDisplayAdapter; + // Viewports of the default display and the display that should receive touch // input from an external source. Used by the input system. private final DisplayViewport mDefaultViewport = new DisplayViewport(); @@ -363,13 +370,7 @@ public final class DisplayManagerService extends IDisplayManager.Stub { synchronized (mSyncRoot) { if (mAllDisplayBlankStateFromPowerManager != DISPLAY_BLANK_STATE_BLANKED) { mAllDisplayBlankStateFromPowerManager = DISPLAY_BLANK_STATE_BLANKED; - - final int count = mDisplayDevices.size(); - for (int i = 0; i < count; i++) { - DisplayDevice device = mDisplayDevices.get(i); - device.blankLocked(); - } - + updateAllDisplayBlankingLocked(); scheduleTraversalLocked(false); } } @@ -382,13 +383,7 @@ public final class DisplayManagerService extends IDisplayManager.Stub { synchronized (mSyncRoot) { if (mAllDisplayBlankStateFromPowerManager != DISPLAY_BLANK_STATE_UNBLANKED) { mAllDisplayBlankStateFromPowerManager = DISPLAY_BLANK_STATE_UNBLANKED; - - final int count = mDisplayDevices.size(); - for (int i = 0; i < count; i++) { - DisplayDevice device = mDisplayDevices.get(i); - device.unblankLocked(); - } - + updateAllDisplayBlankingLocked(); scheduleTraversalLocked(false); } } @@ -403,12 +398,21 @@ public final class DisplayManagerService extends IDisplayManager.Stub { */ @Override // Binder call public DisplayInfo getDisplayInfo(int displayId) { - synchronized (mSyncRoot) { - LogicalDisplay display = mLogicalDisplays.get(displayId); - if (display != null) { - return display.getDisplayInfoLocked(); + final int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mSyncRoot) { + LogicalDisplay display = mLogicalDisplays.get(displayId); + if (display != null) { + DisplayInfo info = display.getDisplayInfoLocked(); + if (info.hasAccess(callingUid)) { + return info; + } + } + return null; } - return null; + } finally { + Binder.restoreCallingIdentity(token); } } @@ -417,13 +421,27 @@ public final class DisplayManagerService extends IDisplayManager.Stub { */ @Override // Binder call public int[] getDisplayIds() { - synchronized (mSyncRoot) { - final int count = mLogicalDisplays.size(); - int[] displayIds = new int[count]; - for (int i = 0; i < count; i++) { - displayIds[i] = mLogicalDisplays.keyAt(i); + final int callingUid = Binder.getCallingUid(); + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mSyncRoot) { + final int count = mLogicalDisplays.size(); + int[] displayIds = new int[count]; + int n = 0; + for (int i = 0; i < count; i++) { + LogicalDisplay display = mLogicalDisplays.valueAt(i); + DisplayInfo info = display.getDisplayInfoLocked(); + if (info.hasAccess(callingUid)) { + displayIds[n++] = mLogicalDisplays.keyAt(i); + } + } + if (n != count) { + displayIds = Arrays.copyOfRange(displayIds, 0, n); + } + return displayIds; } - return displayIds; + } finally { + Binder.restoreCallingIdentity(token); } } @@ -570,6 +588,95 @@ public final class DisplayManagerService extends IDisplayManager.Stub { == PackageManager.PERMISSION_GRANTED; } + @Override // Binder call + public int createPrivateVirtualDisplay(IBinder appToken, String packageName, + String name, int width, int height, int densityDpi, Surface surface) { + final int callingUid = Binder.getCallingUid(); + if (!validatePackageName(callingUid, packageName)) { + throw new SecurityException("packageName must match the calling uid"); + } + if (appToken == null) { + throw new IllegalArgumentException("appToken must not be null"); + } + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("name must be non-null and non-empty"); + } + if (width <= 0 || height <= 0 || densityDpi <= 0) { + throw new IllegalArgumentException("width, height, and densityDpi must be " + + "greater than 0"); + } + if (surface == null) { + throw new IllegalArgumentException("surface must not be null"); + } + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mSyncRoot) { + if (mVirtualDisplayAdapter == null) { + Slog.w(TAG, "Rejecting request to create private virtual display " + + "because the virtual display adapter is not available."); + return -1; + } + + DisplayDevice device = mVirtualDisplayAdapter.createPrivateVirtualDisplayLocked( + appToken, callingUid, packageName, name, width, height, densityDpi, + surface); + if (device == null) { + return -1; + } + + handleDisplayDeviceAddedLocked(device); + LogicalDisplay display = findLogicalDisplayForDeviceLocked(device); + if (display != null) { + return display.getDisplayIdLocked(); + } + + // Something weird happened and the logical display was not created. + Slog.w(TAG, "Rejecting request to create private virtual display " + + "because the logical display was not created."); + mVirtualDisplayAdapter.releaseVirtualDisplayLocked(appToken); + handleDisplayDeviceRemovedLocked(device); + } + } finally { + Binder.restoreCallingIdentity(token); + } + return -1; + } + + @Override // Binder call + public void releaseVirtualDisplay(IBinder appToken) { + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mSyncRoot) { + if (mVirtualDisplayAdapter == null) { + return; + } + + DisplayDevice device = + mVirtualDisplayAdapter.releaseVirtualDisplayLocked(appToken); + if (device != null) { + handleDisplayDeviceRemovedLocked(device); + } + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private boolean validatePackageName(int uid, String packageName) { + if (packageName != null) { + String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid); + if (packageNames != null) { + for (String n : packageNames) { + if (n.equals(packageName)) { + return true; + } + } + } + } + return false; + } + private void registerDefaultDisplayAdapter() { // Register default display adapter. synchronized (mSyncRoot) { @@ -588,6 +695,7 @@ public final class DisplayManagerService extends IDisplayManager.Stub { if (shouldRegisterNonEssentialDisplayAdaptersLocked()) { registerOverlayDisplayAdapterLocked(); registerWifiDisplayAdapterLocked(); + registerVirtualDisplayAdapterLocked(); } } } @@ -608,6 +716,12 @@ public final class DisplayManagerService extends IDisplayManager.Stub { } } + private void registerVirtualDisplayAdapterLocked() { + mVirtualDisplayAdapter = new VirtualDisplayAdapter( + mSyncRoot, mContext, mHandler, mDisplayAdapterListener); + registerDisplayAdapterLocked(mVirtualDisplayAdapter); + } + private boolean shouldRegisterNonEssentialDisplayAdaptersLocked() { // In safe mode, we disable non-essential display adapters to give the user // an opportunity to fix broken settings or other problems that might affect @@ -625,29 +739,23 @@ public final class DisplayManagerService extends IDisplayManager.Stub { private void handleDisplayDeviceAdded(DisplayDevice device) { synchronized (mSyncRoot) { - if (mDisplayDevices.contains(device)) { - Slog.w(TAG, "Attempted to add already added display device: " - + device.getDisplayDeviceInfoLocked()); - return; - } + handleDisplayDeviceAddedLocked(device); + } + } - Slog.i(TAG, "Display device added: " + device.getDisplayDeviceInfoLocked()); + private void handleDisplayDeviceAddedLocked(DisplayDevice device) { + if (mDisplayDevices.contains(device)) { + Slog.w(TAG, "Attempted to add already added display device: " + + device.getDisplayDeviceInfoLocked()); + return; + } - mDisplayDevices.add(device); - addLogicalDisplayLocked(device); - scheduleTraversalLocked(false); + Slog.i(TAG, "Display device added: " + device.getDisplayDeviceInfoLocked()); - // Blank or unblank the display immediately to match the state requested - // by the power manager (if known). - switch (mAllDisplayBlankStateFromPowerManager) { - case DISPLAY_BLANK_STATE_BLANKED: - device.blankLocked(); - break; - case DISPLAY_BLANK_STATE_UNBLANKED: - device.unblankLocked(); - break; - } - } + mDisplayDevices.add(device); + addLogicalDisplayLocked(device); + updateDisplayBlankingLocked(device); + scheduleTraversalLocked(false); } private void handleDisplayDeviceChanged(DisplayDevice device) { @@ -669,17 +777,44 @@ public final class DisplayManagerService extends IDisplayManager.Stub { private void handleDisplayDeviceRemoved(DisplayDevice device) { synchronized (mSyncRoot) { - if (!mDisplayDevices.remove(device)) { - Slog.w(TAG, "Attempted to remove non-existent display device: " - + device.getDisplayDeviceInfoLocked()); - return; - } + handleDisplayDeviceRemovedLocked(device); + } + } + private void handleDisplayDeviceRemovedLocked(DisplayDevice device) { + if (!mDisplayDevices.remove(device)) { + Slog.w(TAG, "Attempted to remove non-existent display device: " + + device.getDisplayDeviceInfoLocked()); + return; + } - Slog.i(TAG, "Display device removed: " + device.getDisplayDeviceInfoLocked()); + Slog.i(TAG, "Display device removed: " + device.getDisplayDeviceInfoLocked()); - mRemovedDisplayDevices.add(device); - updateLogicalDisplaysLocked(); - scheduleTraversalLocked(false); + mRemovedDisplayDevices.add(device); + updateLogicalDisplaysLocked(); + scheduleTraversalLocked(false); + } + + private void updateAllDisplayBlankingLocked() { + final int count = mDisplayDevices.size(); + for (int i = 0; i < count; i++) { + DisplayDevice device = mDisplayDevices.get(i); + updateDisplayBlankingLocked(device); + } + } + + private void updateDisplayBlankingLocked(DisplayDevice device) { + // Blank or unblank the display immediately to match the state requested + // by the power manager (if known). + DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); + if ((info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) { + switch (mAllDisplayBlankStateFromPowerManager) { + case DISPLAY_BLANK_STATE_BLANKED: + device.blankLocked(); + break; + case DISPLAY_BLANK_STATE_UNBLANKED: + device.unblankLocked(); + break; + } } } @@ -812,13 +947,21 @@ public final class DisplayManagerService extends IDisplayManager.Stub { } private void configureDisplayInTransactionLocked(DisplayDevice device) { + DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); + boolean isPrivate = (info.flags & DisplayDeviceInfo.FLAG_PRIVATE) != 0; + // Find the logical display that the display device is showing. + // Private displays never mirror other displays. LogicalDisplay display = findLogicalDisplayForDeviceLocked(device); - if (display != null && !display.hasContentLocked()) { - display = null; - } - if (display == null) { - display = mLogicalDisplays.get(Display.DEFAULT_DISPLAY); + if (!isPrivate) { + if (display != null && !display.hasContentLocked()) { + // If the display does not have any content of its own, then + // automatically mirror the default logical display contents. + display = null; + } + if (display == null) { + display = mLogicalDisplays.get(Display.DEFAULT_DISPLAY); + } } // Apply the logical display configuration to the display device. @@ -828,11 +971,11 @@ public final class DisplayManagerService extends IDisplayManager.Stub { + device.getDisplayDeviceInfoLocked()); return; } - boolean isBlanked = (mAllDisplayBlankStateFromPowerManager == DISPLAY_BLANK_STATE_BLANKED); + boolean isBlanked = (mAllDisplayBlankStateFromPowerManager == DISPLAY_BLANK_STATE_BLANKED) + && (info.flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0; display.configureDisplayInTransactionLocked(device, isBlanked); // Update the viewports if needed. - DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); if (!mDefaultViewport.valid && (info.flags & DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY) != 0) { setViewportLocked(mDefaultViewport, display, device); diff --git a/services/java/com/android/server/display/LogicalDisplay.java b/services/java/com/android/server/display/LogicalDisplay.java index 424ec36..775ebb2 100644 --- a/services/java/com/android/server/display/LogicalDisplay.java +++ b/services/java/com/android/server/display/LogicalDisplay.java @@ -202,6 +202,9 @@ final class LogicalDisplay { if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_SECURE) != 0) { mBaseDisplayInfo.flags |= Display.FLAG_SECURE; } + if ((deviceInfo.flags & DisplayDeviceInfo.FLAG_PRIVATE) != 0) { + mBaseDisplayInfo.flags |= Display.FLAG_PRIVATE; + } mBaseDisplayInfo.type = deviceInfo.type; mBaseDisplayInfo.address = deviceInfo.address; mBaseDisplayInfo.name = deviceInfo.name; @@ -218,6 +221,8 @@ final class LogicalDisplay { mBaseDisplayInfo.smallestNominalAppHeight = deviceInfo.height; mBaseDisplayInfo.largestNominalAppWidth = deviceInfo.width; mBaseDisplayInfo.largestNominalAppHeight = deviceInfo.height; + mBaseDisplayInfo.ownerUid = deviceInfo.ownerUid; + mBaseDisplayInfo.ownerPackageName = deviceInfo.ownerPackageName; mPrimaryDisplayDeviceInfo = deviceInfo; mInfo = null; diff --git a/services/java/com/android/server/display/VirtualDisplayAdapter.java b/services/java/com/android/server/display/VirtualDisplayAdapter.java new file mode 100644 index 0000000..634fba7 --- /dev/null +++ b/services/java/com/android/server/display/VirtualDisplayAdapter.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display; + +import android.content.Context; +import android.os.Handler; +import android.os.IBinder; +import android.os.IBinder.DeathRecipient; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceControl; + +/** + * A display adapter that provides virtual displays on behalf of applications. + * <p> + * Display adapters are guarded by the {@link DisplayManagerService.SyncRoot} lock. + * </p> + */ +final class VirtualDisplayAdapter extends DisplayAdapter { + static final String TAG = "VirtualDisplayAdapter"; + static final boolean DEBUG = false; + + private final ArrayMap<IBinder, VirtualDisplayDevice> mVirtualDisplayDevices = + new ArrayMap<IBinder, VirtualDisplayDevice>(); + + // Called with SyncRoot lock held. + public VirtualDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, + Context context, Handler handler, Listener listener) { + super(syncRoot, context, handler, listener, TAG); + } + + public DisplayDevice createPrivateVirtualDisplayLocked(IBinder appToken, + int ownerUid, String ownerPackageName, + String name, int width, int height, int densityDpi, Surface surface) { + IBinder displayToken = SurfaceControl.createDisplay(name, false /*secure*/); + VirtualDisplayDevice device = new VirtualDisplayDevice(displayToken, appToken, + ownerUid, ownerPackageName, name, width, height, densityDpi, surface); + + try { + appToken.linkToDeath(device, 0); + } catch (RemoteException ex) { + device.releaseLocked(); + return null; + } + + mVirtualDisplayDevices.put(appToken, device); + + // Return the display device without actually sending the event indicating + // that it was added. The caller will handle it. + return device; + } + + public DisplayDevice releaseVirtualDisplayLocked(IBinder appToken) { + VirtualDisplayDevice device = mVirtualDisplayDevices.remove(appToken); + if (device != null) { + appToken.unlinkToDeath(device, 0); + } + + // Return the display device that was removed without actually sending the + // event indicating that it was removed. The caller will handle it. + return device; + } + + private void handleBinderDiedLocked(IBinder appToken) { + VirtualDisplayDevice device = mVirtualDisplayDevices.remove(appToken); + if (device != null) { + Slog.i(TAG, "Virtual display device released because application token died: " + + device.mOwnerPackageName); + sendDisplayDeviceEventLocked(device, DISPLAY_DEVICE_EVENT_REMOVED); + } + } + + private final class VirtualDisplayDevice extends DisplayDevice + implements DeathRecipient { + private final IBinder mAppToken; + private final int mOwnerUid; + final String mOwnerPackageName; + private final String mName; + private final int mWidth; + private final int mHeight; + private final int mDensityDpi; + + private boolean mReleased; + private Surface mSurface; + private DisplayDeviceInfo mInfo; + + public VirtualDisplayDevice(IBinder displayToken, + IBinder appToken, int ownerUid, String ownerPackageName, + String name, int width, int height, int densityDpi, Surface surface) { + super(VirtualDisplayAdapter.this, displayToken); + mAppToken = appToken; + mOwnerUid = ownerUid; + mOwnerPackageName = ownerPackageName; + mName = name; + mWidth = width; + mHeight = height; + mDensityDpi = densityDpi; + mSurface = surface; + } + + @Override + public void binderDied() { + synchronized (getSyncRoot()) { + if (!mReleased) { + handleBinderDiedLocked(mAppToken); + } + } + } + + public void releaseLocked() { + mReleased = true; + sendTraversalRequestLocked(); + } + + @Override + public void performTraversalInTransactionLocked() { + if (mReleased && mSurface != null) { + mSurface.destroy(); + mSurface = null; + } + setSurfaceInTransactionLocked(mSurface); + } + + @Override + public DisplayDeviceInfo getDisplayDeviceInfoLocked() { + if (mInfo == null) { + mInfo = new DisplayDeviceInfo(); + mInfo.name = mName; + mInfo.width = mWidth; + mInfo.height = mHeight; + mInfo.refreshRate = 60; + mInfo.densityDpi = mDensityDpi; + mInfo.xDpi = mDensityDpi; + mInfo.yDpi = mDensityDpi; + mInfo.flags = DisplayDeviceInfo.FLAG_PRIVATE | DisplayDeviceInfo.FLAG_NEVER_BLANK; + mInfo.type = Display.TYPE_VIRTUAL; + mInfo.touch = DisplayDeviceInfo.TOUCH_NONE; + mInfo.ownerUid = mOwnerUid; + mInfo.ownerPackageName = mOwnerPackageName; + } + return mInfo; + } + } +} diff --git a/services/java/com/android/server/wm/DisplayContent.java b/services/java/com/android/server/wm/DisplayContent.java index 82e8c7f..feac370 100644 --- a/services/java/com/android/server/wm/DisplayContent.java +++ b/services/java/com/android/server/wm/DisplayContent.java @@ -140,6 +140,13 @@ class DisplayContent { return mDisplayInfo; } + /** + * Returns true if the specified UID has access to this display. + */ + public boolean hasAccess(int uid) { + return mDisplay.hasAccess(uid); + } + boolean homeOnTop() { return mStackBoxes.get(0).mStack != mHomeStack; } diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java index faeb37c..fd06535 100644 --- a/services/java/com/android/server/wm/WindowManagerService.java +++ b/services/java/com/android/server/wm/WindowManagerService.java @@ -2105,6 +2105,13 @@ public class WindowManagerService extends IWindowManager.Stub final DisplayContent displayContent = getDisplayContentLocked(displayId); if (displayContent == null) { + Slog.w(TAG, "Attempted to add window to a display that does not exist: " + + displayId + ". Aborting."); + return WindowManagerGlobal.ADD_INVALID_DISPLAY; + } + if (!displayContent.hasAccess(session.mUid)) { + Slog.w(TAG, "Attempted to add window to a display for which the application " + + "does not have access: " + displayId + ". Aborting."); return WindowManagerGlobal.ADD_INVALID_DISPLAY; } @@ -3232,8 +3239,7 @@ public class WindowManagerService extends IWindowManager.Stub if (taskNdx >= 0 || t >= 0) { Slog.w(TAG, "validateAppTokens: Mismatch! ActivityManager=" + tasks); - Slog.w(TAG, "validateAppTokens: Mismatch! WindowManager=" - + displayContent.getTasks()); + Slog.w(TAG, "validateAppTokens: Mismatch! WindowManager=" + localTasks); Slog.w(TAG, "validateAppTokens: Mismatch! Callers=" + Debug.getCallers(4)); } } @@ -4838,6 +4844,7 @@ public class WindowManagerService extends IWindowManager.Stub synchronized (mWindowMap) { Task task = mTaskIdToTask.get(taskId); if (task == null) { + if (DEBUG_STACK) Slog.i(TAG, "removeTask: could not find taskId=" + taskId); return; } final TaskStack stack = task.mStack; @@ -7521,7 +7528,7 @@ public class WindowManagerService extends IWindowManager.Stub public void getInitialDisplaySize(int displayId, Point size) { synchronized (mWindowMap) { final DisplayContent displayContent = getDisplayContentLocked(displayId); - if (displayContent != null) { + if (displayContent != null && displayContent.hasAccess(Binder.getCallingUid())) { synchronized(displayContent.mDisplaySizeLock) { size.x = displayContent.mInitialDisplayWidth; size.y = displayContent.mInitialDisplayHeight; @@ -7534,7 +7541,7 @@ public class WindowManagerService extends IWindowManager.Stub public void getBaseDisplaySize(int displayId, Point size) { synchronized (mWindowMap) { final DisplayContent displayContent = getDisplayContentLocked(displayId); - if (displayContent != null) { + if (displayContent != null && displayContent.hasAccess(Binder.getCallingUid())) { synchronized(displayContent.mDisplaySizeLock) { size.x = displayContent.mBaseDisplayWidth; size.y = displayContent.mBaseDisplayHeight; @@ -7649,7 +7656,7 @@ public class WindowManagerService extends IWindowManager.Stub public int getInitialDisplayDensity(int displayId) { synchronized (mWindowMap) { final DisplayContent displayContent = getDisplayContentLocked(displayId); - if (displayContent != null) { + if (displayContent != null && displayContent.hasAccess(Binder.getCallingUid())) { synchronized(displayContent.mDisplaySizeLock) { return displayContent.mInitialDisplayDensity; } @@ -7662,7 +7669,7 @@ public class WindowManagerService extends IWindowManager.Stub public int getBaseDisplayDensity(int displayId) { synchronized (mWindowMap) { final DisplayContent displayContent = getDisplayContentLocked(displayId); - if (displayContent != null) { + if (displayContent != null && displayContent.hasAccess(Binder.getCallingUid())) { synchronized(displayContent.mDisplaySizeLock) { return displayContent.mBaseDisplayDensity; } diff --git a/tests/AccessoryDisplay/Android.mk b/tests/AccessoryDisplay/Android.mk new file mode 100644 index 0000000..85cb309 --- /dev/null +++ b/tests/AccessoryDisplay/Android.mk @@ -0,0 +1,17 @@ +# Copyright (C) 2013 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 $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tests/AccessoryDisplay/README b/tests/AccessoryDisplay/README new file mode 100644 index 0000000..5ce558c --- /dev/null +++ b/tests/AccessoryDisplay/README @@ -0,0 +1,50 @@ +This directory contains sample code to test the use of virtual +displays created over an Android Open Accessories Protocol link. + +--- DESCRIPTION --- + +There are two applications with two distinct roles: a sink +and a source. + +1. Sink Application + +The role of the sink is to emulate an external display that happens +to be connected using the USB accessory protocol. Think of it as +a monitor or video dock that the user will want to plug a phone into. + +The sink application uses the UsbDevice APIs to receive connections +from the source device over USB. The sink acts as a USB host +in this arrangement and will provide power to the source. + +The sink application decodes encoded video from the source and +displays it in a SurfaceView. The sink also injects passes touch +events to the source over USB HID. + +2. Source Application + +The role of the source is to present some content onto an external +display that happens to be attached over USB. This is the typical +role that a phone or tablet might have when the user is trying to +play content to an external monitor. + +The source application uses the UsbAccessory APIs to connect +to the sink device over USB. The source acts as a USB peripheral +in this arrangement and will receive power from the sink. + +The source application uses the DisplayManager APIs to create +a private virtual display which passes the framebuffer through +an encoder and streams the output to the sink over USB. Then +the application opens a Presentation on the new virtual display +and shows a silly cube animation. + +--- USAGE --- + +These applications should be installed on two separate Android +devices which are then connected using a USB OTG cable. +Remember that the sink device is functioning as the USB host +so the USB OTG cable should be plugged directly into it. + +When connected, the applications should automatically launch +on each device. The source will then begin to project display +contents to the sink. + diff --git a/tests/AccessoryDisplay/common/Android.mk b/tests/AccessoryDisplay/common/Android.mk new file mode 100644 index 0000000..2d4de15 --- /dev/null +++ b/tests/AccessoryDisplay/common/Android.mk @@ -0,0 +1,23 @@ +# Copyright (C) 2013 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) + +# Build the application. +include $(CLEAR_VARS) +LOCAL_MODULE := AccessoryDisplayCommon +LOCAL_MODULE_TAGS := tests +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under, src) +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/BufferPool.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/BufferPool.java new file mode 100644 index 0000000..a6bb5c1 --- /dev/null +++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/BufferPool.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.common; + +import java.nio.ByteBuffer; + +/** + * Maintains a bounded pool of buffers. Attempts to acquire buffers beyond the maximum + * count will block until other buffers are released. + */ +final class BufferPool { + private final int mInitialBufferSize; + private final int mMaxBufferSize; + private final ByteBuffer[] mBuffers; + private int mAllocated; + private int mAvailable; + + public BufferPool(int initialBufferSize, int maxBufferSize, int maxBuffers) { + mInitialBufferSize = initialBufferSize; + mMaxBufferSize = maxBufferSize; + mBuffers = new ByteBuffer[maxBuffers]; + } + + public ByteBuffer acquire(int needed) { + synchronized (this) { + for (;;) { + if (mAvailable != 0) { + mAvailable -= 1; + return grow(mBuffers[mAvailable], needed); + } + + if (mAllocated < mBuffers.length) { + mAllocated += 1; + return ByteBuffer.allocate(chooseCapacity(mInitialBufferSize, needed)); + } + + try { + wait(); + } catch (InterruptedException ex) { + } + } + } + } + + public void release(ByteBuffer buffer) { + synchronized (this) { + buffer.clear(); + mBuffers[mAvailable++] = buffer; + notifyAll(); + } + } + + public ByteBuffer grow(ByteBuffer buffer, int needed) { + int capacity = buffer.capacity(); + if (capacity < needed) { + final ByteBuffer oldBuffer = buffer; + capacity = chooseCapacity(capacity, needed); + buffer = ByteBuffer.allocate(capacity); + oldBuffer.flip(); + buffer.put(oldBuffer); + } + return buffer; + } + + private int chooseCapacity(int capacity, int needed) { + while (capacity < needed) { + capacity *= 2; + } + if (capacity > mMaxBufferSize) { + if (needed > mMaxBufferSize) { + throw new IllegalArgumentException("Requested size " + needed + + " is larger than maximum buffer size " + mMaxBufferSize + "."); + } + capacity = mMaxBufferSize; + } + return capacity; + } +} diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Logger.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Logger.java new file mode 100644 index 0000000..e0b7e82 --- /dev/null +++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Logger.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.common; + +public abstract class Logger { + public abstract void log(String message); + + public void logError(String message) { + log("ERROR: " + message); + } +} diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Protocol.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Protocol.java new file mode 100644 index 0000000..46fee32 --- /dev/null +++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Protocol.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.common; + +/** + * Defines message types. + */ +public class Protocol { + // Message header. + // 0: service id (16 bits) + // 2: what (16 bits) + // 4: content size (32 bits) + // 8: ... content follows ... + static final int HEADER_SIZE = 8; + + // Maximum size of a message envelope including the header and contents. + static final int MAX_ENVELOPE_SIZE = 64 * 1024; + + /** + * Maximum message content size. + */ + public static final int MAX_CONTENT_SIZE = MAX_ENVELOPE_SIZE - HEADER_SIZE; + + public static final class DisplaySinkService { + private DisplaySinkService() { } + + public static final int ID = 1; + + // Query sink capabilities. + // Replies with sink available or not available. + public static final int MSG_QUERY = 1; + + // Send MPEG2-TS H.264 encoded content. + public static final int MSG_CONTENT = 2; + } + + public static final class DisplaySourceService { + private DisplaySourceService() { } + + public static final int ID = 2; + + // Sink is now available for use. + // 0: width (32 bits) + // 4: height (32 bits) + // 8: density dpi (32 bits) + public static final int MSG_SINK_AVAILABLE = 1; + + // Sink is no longer available for use. + public static final int MSG_SINK_NOT_AVAILABLE = 2; + } +} diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Service.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Service.java new file mode 100644 index 0000000..70b3806 --- /dev/null +++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Service.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.common; + +import com.android.accessorydisplay.common.Transport; + +import android.content.Context; +import android.os.Looper; + +import java.nio.ByteBuffer; + +/** + * Base implementation of a service that communicates over a transport. + * <p> + * This object's interface is single-threaded. It is only intended to be + * accessed from the {@link Looper} thread on which the transport was created. + * </p> + */ +public abstract class Service implements Transport.Callback { + private final Context mContext; + private final Transport mTransport; + private final int mServiceId; + + public Service(Context context, Transport transport, int serviceId) { + mContext = context; + mTransport = transport; + mServiceId = serviceId; + } + + public Context getContext() { + return mContext; + } + + public int getServiceId() { + return mServiceId; + } + + public Transport getTransport() { + return mTransport; + } + + public Logger getLogger() { + return mTransport.getLogger(); + } + + public void start() { + mTransport.registerService(mServiceId, this); + } + + public void stop() { + mTransport.unregisterService(mServiceId); + } + + @Override + public void onMessageReceived(int service, int what, ByteBuffer content) { + } +} diff --git a/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Transport.java b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Transport.java new file mode 100644 index 0000000..84897d3 --- /dev/null +++ b/tests/AccessoryDisplay/common/src/com/android/accessorydisplay/common/Transport.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.common; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.SparseArray; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A simple message transport. + * <p> + * This object's interface is thread-safe, however incoming messages + * are always delivered on the {@link Looper} thread on which the transport + * was created. + * </p> + */ +public abstract class Transport { + private static final int MAX_INPUT_BUFFERS = 8; + + private final Logger mLogger; + + // The transport thread looper and handler. + private final TransportHandler mHandler; + + // Lock to guard all mutable state. + private final Object mLock = new Object(); + + // The output buffer. Set to null when the transport is closed. + private ByteBuffer mOutputBuffer; + + // The input buffer pool. + private BufferPool mInputBufferPool; + + // The reader thread. Initialized when reading starts. + private ReaderThread mThread; + + // The list of callbacks indexed by service id. + private final SparseArray<Callback> mServices = new SparseArray<Callback>(); + + public Transport(Logger logger, int maxPacketSize) { + mLogger = logger; + mHandler = new TransportHandler(); + mOutputBuffer = ByteBuffer.allocate(maxPacketSize); + mInputBufferPool = new BufferPool( + maxPacketSize, Protocol.MAX_ENVELOPE_SIZE, MAX_INPUT_BUFFERS); + } + + /** + * Gets the logger for debugging. + */ + public Logger getLogger() { + return mLogger; + } + + /** + * Gets the handler on the transport's thread. + */ + public Handler getHandler() { + return mHandler; + } + + /** + * Closes the transport. + */ + public void close() { + synchronized (mLock) { + if (mOutputBuffer != null) { + if (mThread == null) { + ioClose(); + } else { + // If the thread was started then it will be responsible for + // closing the stream when it quits because it may currently + // be in the process of reading from the stream so we can't simply + // shut it down right now. + mThread.quit(); + } + mOutputBuffer = null; + } + } + } + + /** + * Sends a message. + * + * @param service The service to whom the message is addressed. + * @param what The message type. + * @param content The content, or null if there is none. + * @return True if the message was sent successfully, false if an error occurred. + */ + public boolean sendMessage(int service, int what, ByteBuffer content) { + checkServiceId(service); + checkMessageId(what); + + try { + synchronized (mLock) { + if (mOutputBuffer == null) { + mLogger.logError("Send message failed because transport was closed."); + return false; + } + + final byte[] outputArray = mOutputBuffer.array(); + final int capacity = mOutputBuffer.capacity(); + mOutputBuffer.clear(); + mOutputBuffer.putShort((short)service); + mOutputBuffer.putShort((short)what); + if (content == null) { + mOutputBuffer.putInt(0); + } else { + final int contentLimit = content.limit(); + int contentPosition = content.position(); + int contentRemaining = contentLimit - contentPosition; + if (contentRemaining > Protocol.MAX_CONTENT_SIZE) { + throw new IllegalArgumentException("Message content too large: " + + contentRemaining + " > " + Protocol.MAX_CONTENT_SIZE); + } + mOutputBuffer.putInt(contentRemaining); + while (contentRemaining != 0) { + final int outputAvailable = capacity - mOutputBuffer.position(); + if (contentRemaining <= outputAvailable) { + mOutputBuffer.put(content); + break; + } + content.limit(contentPosition + outputAvailable); + mOutputBuffer.put(content); + content.limit(contentLimit); + ioWrite(outputArray, 0, capacity); + contentPosition += outputAvailable; + contentRemaining -= outputAvailable; + mOutputBuffer.clear(); + } + } + ioWrite(outputArray, 0, mOutputBuffer.position()); + return true; + } + } catch (IOException ex) { + mLogger.logError("Send message failed: " + ex); + return false; + } + } + + /** + * Starts reading messages on a separate thread. + */ + public void startReading() { + synchronized (mLock) { + if (mOutputBuffer == null) { + throw new IllegalStateException("Transport has been closed"); + } + + mThread = new ReaderThread(); + mThread.start(); + } + } + + /** + * Registers a service and provides a callback to receive messages. + * + * @param service The service id. + * @param callback The callback to use. + */ + public void registerService(int service, Callback callback) { + checkServiceId(service); + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + + synchronized (mLock) { + mServices.put(service, callback); + } + } + + /** + * Unregisters a service. + * + * @param service The service to unregister. + */ + public void unregisterService(int service) { + checkServiceId(service); + + synchronized (mLock) { + mServices.remove(service); + } + } + + private void dispatchMessageReceived(int service, int what, ByteBuffer content) { + final Callback callback; + synchronized (mLock) { + callback = mServices.get(service); + } + if (callback != null) { + callback.onMessageReceived(service, what, content); + } else { + mLogger.log("Discarding message " + what + + " for unregistered service " + service); + } + } + + private static void checkServiceId(int service) { + if (service < 0 || service > 0xffff) { + throw new IllegalArgumentException("service id out of range: " + service); + } + } + + private static void checkMessageId(int what) { + if (what < 0 || what > 0xffff) { + throw new IllegalArgumentException("message id out of range: " + what); + } + } + + // The IO methods must be safe to call on any thread. + // They may be called concurrently. + protected abstract void ioClose(); + protected abstract int ioRead(byte[] buffer, int offset, int count) + throws IOException; + protected abstract void ioWrite(byte[] buffer, int offset, int count) + throws IOException; + + /** + * Callback for services that handle received messages. + */ + public interface Callback { + /** + * Indicates that a message was received. + * + * @param service The service to whom the message is addressed. + * @param what The message type. + * @param content The content, or null if there is none. + */ + public void onMessageReceived(int service, int what, ByteBuffer content); + } + + final class TransportHandler extends Handler { + @Override + public void handleMessage(Message msg) { + final ByteBuffer buffer = (ByteBuffer)msg.obj; + try { + final int limit = buffer.limit(); + while (buffer.position() < limit) { + final int service = buffer.getShort() & 0xffff; + final int what = buffer.getShort() & 0xffff; + final int contentSize = buffer.getInt(); + if (contentSize == 0) { + dispatchMessageReceived(service, what, null); + } else { + final int end = buffer.position() + contentSize; + buffer.limit(end); + dispatchMessageReceived(service, what, buffer); + buffer.limit(limit); + buffer.position(end); + } + } + } finally { + mInputBufferPool.release(buffer); + } + } + } + + final class ReaderThread extends Thread { + // Set to true when quitting. + private volatile boolean mQuitting; + + public ReaderThread() { + super("Accessory Display Transport"); + } + + @Override + public void run() { + loop(); + ioClose(); + } + + private void loop() { + ByteBuffer buffer = null; + int length = Protocol.HEADER_SIZE; + int contentSize = -1; + outer: while (!mQuitting) { + // Get a buffer. + if (buffer == null) { + buffer = mInputBufferPool.acquire(length); + } else { + buffer = mInputBufferPool.grow(buffer, length); + } + + // Read more data until needed number of bytes obtained. + int position = buffer.position(); + int count; + try { + count = ioRead(buffer.array(), position, buffer.capacity() - position); + if (count < 0) { + break; // end of stream + } + } catch (IOException ex) { + mLogger.logError("Read failed: " + ex); + break; // error + } + position += count; + buffer.position(position); + if (contentSize < 0 && position >= Protocol.HEADER_SIZE) { + contentSize = buffer.getInt(4); + if (contentSize < 0 || contentSize > Protocol.MAX_CONTENT_SIZE) { + mLogger.logError("Encountered invalid content size: " + contentSize); + break; // malformed stream + } + length += contentSize; + } + if (position < length) { + continue; // need more data + } + + // There is at least one complete message in the buffer. + // Find the end of a contiguous chunk of complete messages. + int next = length; + int remaining; + for (;;) { + length = Protocol.HEADER_SIZE; + remaining = position - next; + if (remaining < length) { + contentSize = -1; + break; // incomplete header, need more data + } + contentSize = buffer.getInt(next + 4); + if (contentSize < 0 || contentSize > Protocol.MAX_CONTENT_SIZE) { + mLogger.logError("Encountered invalid content size: " + contentSize); + break outer; // malformed stream + } + length += contentSize; + if (remaining < length) { + break; // incomplete content, need more data + } + next += length; + } + + // Post the buffer then don't modify it anymore. + // Now this is kind of sneaky. We know that no other threads will + // be acquiring buffers from the buffer pool so we can keep on + // referring to this buffer as long as we don't modify its contents. + // This allows us to operate in a single-buffered mode if desired. + buffer.limit(next); + buffer.rewind(); + mHandler.obtainMessage(0, buffer).sendToTarget(); + + // If there is an incomplete message at the end, then we will need + // to copy it to a fresh buffer before continuing. In the single-buffered + // case, we may acquire the same buffer as before which is fine. + if (remaining == 0) { + buffer = null; + } else { + final ByteBuffer oldBuffer = buffer; + buffer = mInputBufferPool.acquire(length); + System.arraycopy(oldBuffer.array(), next, buffer.array(), 0, remaining); + buffer.position(remaining); + } + } + + if (buffer != null) { + mInputBufferPool.release(buffer); + } + } + + public void quit() { + mQuitting = true; + } + } +} diff --git a/tests/AccessoryDisplay/sink/Android.mk b/tests/AccessoryDisplay/sink/Android.mk new file mode 100644 index 0000000..772ce0c --- /dev/null +++ b/tests/AccessoryDisplay/sink/Android.mk @@ -0,0 +1,25 @@ +# Copyright (C) 2013 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) + +# Build the application. +include $(CLEAR_VARS) +LOCAL_PACKAGE_NAME := AccessoryDisplaySink +LOCAL_MODULE_TAGS := tests +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_RESOURCE_DIR = $(LOCAL_PATH)/res +LOCAL_STATIC_JAVA_LIBRARIES := AccessoryDisplayCommon +include $(BUILD_PACKAGE) diff --git a/tests/AccessoryDisplay/sink/AndroidManifest.xml b/tests/AccessoryDisplay/sink/AndroidManifest.xml new file mode 100644 index 0000000..72d498f --- /dev/null +++ b/tests/AccessoryDisplay/sink/AndroidManifest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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.accessorydisplay.sink" > + + <uses-feature android:name="android.hardware.usb.host"/> + <uses-sdk android:minSdkVersion="18" /> + + <application android:label="@string/app_name" + android:icon="@drawable/ic_app" + android:hardwareAccelerated="true"> + + <activity android:name=".SinkActivity" + android:label="@string/app_name" + android:theme="@android:style/Theme.Holo"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" + android:resource="@xml/usb_device_filter"/> + </activity> + + </application> +</manifest> diff --git a/tests/AccessoryDisplay/sink/res/drawable-hdpi/ic_app.png b/tests/AccessoryDisplay/sink/res/drawable-hdpi/ic_app.png Binary files differnew file mode 100755 index 0000000..66a1984 --- /dev/null +++ b/tests/AccessoryDisplay/sink/res/drawable-hdpi/ic_app.png diff --git a/tests/AccessoryDisplay/sink/res/drawable-mdpi/ic_app.png b/tests/AccessoryDisplay/sink/res/drawable-mdpi/ic_app.png Binary files differnew file mode 100644 index 0000000..5ae7701 --- /dev/null +++ b/tests/AccessoryDisplay/sink/res/drawable-mdpi/ic_app.png diff --git a/tests/AccessoryDisplay/sink/res/layout/sink_activity.xml b/tests/AccessoryDisplay/sink/res/layout/sink_activity.xml new file mode 100644 index 0000000..6afb850 --- /dev/null +++ b/tests/AccessoryDisplay/sink/res/layout/sink_activity.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal"> + <LinearLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical"> + <TextView android:id="@+id/fpsTextView" + android:layout_width="match_parent" + android:layout_height="64dp" + android:padding="4dp" /> + + <TextView android:id="@+id/logTextView" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + </LinearLayout> + + <FrameLayout + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="2"> + <SurfaceView android:id="@+id/surfaceView" + android:layout_width="640px" + android:layout_height="480px" /> + </FrameLayout> +</LinearLayout> diff --git a/tests/AccessoryDisplay/sink/res/values/strings.xml b/tests/AccessoryDisplay/sink/res/values/strings.xml new file mode 100644 index 0000000..29cd001 --- /dev/null +++ b/tests/AccessoryDisplay/sink/res/values/strings.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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="app_name">Accessory Display Sink</string> +</resources> diff --git a/tests/AccessoryDisplay/sink/res/xml/usb_device_filter.xml b/tests/AccessoryDisplay/sink/res/xml/usb_device_filter.xml new file mode 100644 index 0000000..e8fe929 --- /dev/null +++ b/tests/AccessoryDisplay/sink/res/xml/usb_device_filter.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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> + <!-- Match all devices --> + <usb-device /> + + <!-- Android USB accessory: accessory --> + <usb-device vendor-id="16601" product-id="11520" /> + <!-- Android USB accessory: accessory + adb --> + <usb-device vendor-id="16601" product-id="11521" /> +</resources> diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/DisplaySinkService.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/DisplaySinkService.java new file mode 100644 index 0000000..daec845 --- /dev/null +++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/DisplaySinkService.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.sink; + +import com.android.accessorydisplay.common.Protocol; +import com.android.accessorydisplay.common.Service; +import com.android.accessorydisplay.common.Transport; + +import android.content.Context; +import android.graphics.Rect; +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaFormat; +import android.os.Handler; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import java.nio.ByteBuffer; + +public class DisplaySinkService extends Service implements SurfaceHolder.Callback { + private final ByteBuffer mBuffer = ByteBuffer.allocate(12); + private final Handler mTransportHandler; + private final int mDensityDpi; + + private SurfaceView mSurfaceView; + + // These fields are guarded by the following lock. + // This is to ensure that the surface lifecycle is respected. Although decoding + // happens on the transport thread, we are not allowed to access the surface after + // it is destroyed by the UI thread so we need to stop the codec immediately. + private final Object mSurfaceAndCodecLock = new Object(); + private Surface mSurface; + private int mSurfaceWidth; + private int mSurfaceHeight; + private MediaCodec mCodec; + private ByteBuffer[] mCodecInputBuffers; + private BufferInfo mCodecBufferInfo; + + public DisplaySinkService(Context context, Transport transport, int densityDpi) { + super(context, transport, Protocol.DisplaySinkService.ID); + mTransportHandler = transport.getHandler(); + mDensityDpi = densityDpi; + } + + public void setSurfaceView(final SurfaceView surfaceView) { + if (mSurfaceView != surfaceView) { + final SurfaceView oldSurfaceView = mSurfaceView; + mSurfaceView = surfaceView; + + if (oldSurfaceView != null) { + oldSurfaceView.post(new Runnable() { + @Override + public void run() { + final SurfaceHolder holder = oldSurfaceView.getHolder(); + holder.removeCallback(DisplaySinkService.this); + updateSurfaceFromUi(null); + } + }); + } + + if (surfaceView != null) { + surfaceView.post(new Runnable() { + @Override + public void run() { + final SurfaceHolder holder = surfaceView.getHolder(); + holder.addCallback(DisplaySinkService.this); + updateSurfaceFromUi(holder); + } + }); + } + } + } + + @Override + public void onMessageReceived(int service, int what, ByteBuffer content) { + switch (what) { + case Protocol.DisplaySinkService.MSG_QUERY: { + getLogger().log("Received MSG_QUERY."); + sendSinkStatus(); + break; + } + + case Protocol.DisplaySinkService.MSG_CONTENT: { + decode(content); + break; + } + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + // Ignore. Wait for surface changed event that follows. + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + updateSurfaceFromUi(holder); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + updateSurfaceFromUi(null); + } + + private void updateSurfaceFromUi(SurfaceHolder holder) { + Surface surface = null; + int width = 0, height = 0; + if (holder != null && !holder.isCreating()) { + surface = holder.getSurface(); + if (surface.isValid()) { + final Rect frame = holder.getSurfaceFrame(); + width = frame.width(); + height = frame.height(); + } else { + surface = null; + } + } + + synchronized (mSurfaceAndCodecLock) { + if (mSurface == surface && mSurfaceWidth == width && mSurfaceHeight == height) { + return; + } + + mSurface = surface; + mSurfaceWidth = width; + mSurfaceHeight = height; + + if (mCodec != null) { + mCodec.stop(); + mCodec = null; + mCodecInputBuffers = null; + mCodecBufferInfo = null; + } + + if (mSurface != null) { + MediaFormat format = MediaFormat.createVideoFormat( + "video/avc", mSurfaceWidth, mSurfaceHeight); + mCodec = MediaCodec.createDecoderByType("video/avc"); + mCodec.configure(format, mSurface, null, 0); + mCodec.start(); + mCodecBufferInfo = new BufferInfo(); + } + + mTransportHandler.post(new Runnable() { + @Override + public void run() { + sendSinkStatus(); + } + }); + } + } + + private void decode(ByteBuffer content) { + if (content == null) { + return; + } + synchronized (mSurfaceAndCodecLock) { + if (mCodec == null) { + return; + } + + while (content.hasRemaining()) { + if (!provideCodecInputLocked(content)) { + getLogger().log("Dropping content because there are no available buffers."); + return; + } + + consumeCodecOutputLocked(); + } + } + } + + private boolean provideCodecInputLocked(ByteBuffer content) { + final int index = mCodec.dequeueInputBuffer(0); + if (index < 0) { + return false; + } + if (mCodecInputBuffers == null) { + mCodecInputBuffers = mCodec.getInputBuffers(); + } + final ByteBuffer buffer = mCodecInputBuffers[index]; + final int capacity = buffer.capacity(); + buffer.clear(); + if (content.remaining() <= capacity) { + buffer.put(content); + } else { + final int limit = content.limit(); + content.limit(content.position() + capacity); + buffer.put(content); + content.limit(limit); + } + buffer.flip(); + mCodec.queueInputBuffer(index, 0, buffer.limit(), 0, 0); + return true; + } + + private void consumeCodecOutputLocked() { + for (;;) { + final int index = mCodec.dequeueOutputBuffer(mCodecBufferInfo, 0); + if (index >= 0) { + mCodec.releaseOutputBuffer(index, true /*render*/); + } else if (index != MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED + && index != MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + break; + } + } + } + + private void sendSinkStatus() { + synchronized (mSurfaceAndCodecLock) { + if (mCodec != null) { + mBuffer.clear(); + mBuffer.putInt(mSurfaceWidth); + mBuffer.putInt(mSurfaceHeight); + mBuffer.putInt(mDensityDpi); + mBuffer.flip(); + getTransport().sendMessage(Protocol.DisplaySourceService.ID, + Protocol.DisplaySourceService.MSG_SINK_AVAILABLE, mBuffer); + } else { + getTransport().sendMessage(Protocol.DisplaySourceService.ID, + Protocol.DisplaySourceService.MSG_SINK_NOT_AVAILABLE, null); + } + } + } +} diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/SinkActivity.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/SinkActivity.java new file mode 100644 index 0000000..6fe2cfb --- /dev/null +++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/SinkActivity.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.sink; + +import com.android.accessorydisplay.common.Logger; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbConstants; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.hardware.usb.UsbInterface; +import android.hardware.usb.UsbManager; +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaFormat; +import android.os.Bundle; +import android.text.method.ScrollingMovementMethod; +import android.util.Log; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.widget.TextView; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.Map; + +public class SinkActivity extends Activity { + private static final String TAG = "SinkActivity"; + + private static final String ACTION_USB_DEVICE_PERMISSION = + "com.android.accessorydisplay.sink.ACTION_USB_DEVICE_PERMISSION"; + + private static final String MANUFACTURER = "Android"; + private static final String MODEL = "Accessory Display"; + private static final String DESCRIPTION = "Accessory Display Sink Test Application"; + private static final String VERSION = "1.0"; + private static final String URI = "http://www.android.com/"; + private static final String SERIAL = "0000000012345678"; + + private static final int MULTITOUCH_DEVICE_ID = 0; + private static final int MULTITOUCH_REPORT_ID = 1; + private static final int MULTITOUCH_MAX_CONTACTS = 1; + + private UsbManager mUsbManager; + private DeviceReceiver mReceiver; + private TextView mLogTextView; + private TextView mFpsTextView; + private SurfaceView mSurfaceView; + private Logger mLogger; + + private boolean mConnected; + private int mProtocolVersion; + private UsbDevice mDevice; + private UsbInterface mAccessoryInterface; + private UsbDeviceConnection mAccessoryConnection; + private UsbEndpoint mControlEndpoint; + private UsbAccessoryBulkTransport mTransport; + + private boolean mAttached; + private DisplaySinkService mDisplaySinkService; + + private final ByteBuffer mHidBuffer = ByteBuffer.allocate(4096); + private UsbHid.Multitouch mMultitouch; + private boolean mMultitouchEnabled; + private UsbHid.Multitouch.Contact[] mMultitouchContacts; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mUsbManager = (UsbManager)getSystemService(Context.USB_SERVICE); + + setContentView(R.layout.sink_activity); + + mLogTextView = (TextView) findViewById(R.id.logTextView); + mLogTextView.setMovementMethod(ScrollingMovementMethod.getInstance()); + mLogger = new TextLogger(); + + mFpsTextView = (TextView) findViewById(R.id.fpsTextView); + + mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView); + mSurfaceView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + sendHidTouch(event); + return true; + } + }); + + mLogger.log("Waiting for accessory display source to be attached to USB..."); + + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); + filter.addAction(ACTION_USB_DEVICE_PERMISSION); + mReceiver = new DeviceReceiver(); + registerReceiver(mReceiver, filter); + + Intent intent = getIntent(); + if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { + UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (device != null) { + onDeviceAttached(device); + } + } else { + Map<String, UsbDevice> devices = mUsbManager.getDeviceList(); + if (devices != null) { + for (UsbDevice device : devices.values()) { + onDeviceAttached(device); + } + } + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + unregisterReceiver(mReceiver); + } + + private void onDeviceAttached(UsbDevice device) { + mLogger.log("USB device attached: " + device); + if (!mConnected) { + connect(device); + } + } + + private void onDeviceDetached(UsbDevice device) { + mLogger.log("USB device detached: " + device); + if (mConnected && device.equals(mDevice)) { + disconnect(); + } + } + + private void connect(UsbDevice device) { + if (mConnected) { + disconnect(); + } + + // Check whether we have permission to access the device. + if (!mUsbManager.hasPermission(device)) { + mLogger.log("Prompting the user for access to the device."); + Intent intent = new Intent(ACTION_USB_DEVICE_PERMISSION); + intent.setPackage(getPackageName()); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + this, 0, intent, PendingIntent.FLAG_ONE_SHOT); + mUsbManager.requestPermission(device, pendingIntent); + return; + } + + // Claim the device. + UsbDeviceConnection conn = mUsbManager.openDevice(device); + if (conn == null) { + mLogger.logError("Could not obtain device connection."); + return; + } + UsbInterface iface = device.getInterface(0); + UsbEndpoint controlEndpoint = iface.getEndpoint(0); + if (!conn.claimInterface(iface, true)) { + mLogger.logError("Could not claim interface."); + return; + } + try { + // If already in accessory mode, then connect to the device. + if (isAccessory(device)) { + mLogger.log("Connecting to accessory..."); + + int protocolVersion = getProtocol(conn); + if (protocolVersion < 1) { + mLogger.logError("Device does not support accessory protocol."); + return; + } + mLogger.log("Protocol version: " + protocolVersion); + + // Setup bulk endpoints. + UsbEndpoint bulkIn = null; + UsbEndpoint bulkOut = null; + for (int i = 0; i < iface.getEndpointCount(); i++) { + UsbEndpoint ep = iface.getEndpoint(i); + if (ep.getDirection() == UsbConstants.USB_DIR_IN) { + if (bulkIn == null) { + mLogger.log(String.format("Bulk IN endpoint: %d", i)); + bulkIn = ep; + } + } else { + if (bulkOut == null) { + mLogger.log(String.format("Bulk OUT endpoint: %d", i)); + bulkOut = ep; + } + } + } + if (bulkIn == null || bulkOut == null) { + mLogger.logError("Unable to find bulk endpoints"); + return; + } + + mLogger.log("Connected"); + mConnected = true; + mDevice = device; + mProtocolVersion = protocolVersion; + mAccessoryInterface = iface; + mAccessoryConnection = conn; + mControlEndpoint = controlEndpoint; + mTransport = new UsbAccessoryBulkTransport(mLogger, conn, bulkIn, bulkOut); + if (mProtocolVersion >= 2) { + registerHid(); + } + startServices(); + mTransport.startReading(); + return; + } + + // Do accessory negotiation. + mLogger.log("Attempting to switch device to accessory mode..."); + + // Send get protocol. + int protocolVersion = getProtocol(conn); + if (protocolVersion < 1) { + mLogger.logError("Device does not support accessory protocol."); + return; + } + mLogger.log("Protocol version: " + protocolVersion); + + // Send identifying strings. + sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_MANUFACTURER, MANUFACTURER); + sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_MODEL, MODEL); + sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_DESCRIPTION, DESCRIPTION); + sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_VERSION, VERSION); + sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_URI, URI); + sendString(conn, UsbAccessoryConstants.ACCESSORY_STRING_SERIAL, SERIAL); + + // Send start. + // The device should re-enumerate as an accessory. + mLogger.log("Sending accessory start request."); + int len = conn.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, + UsbAccessoryConstants.ACCESSORY_START, 0, 0, null, 0, 10000); + if (len != 0) { + mLogger.logError("Device refused to switch to accessory mode."); + } else { + mLogger.log("Waiting for device to re-enumerate..."); + } + } finally { + if (!mConnected) { + conn.releaseInterface(iface); + } + } + } + + private void disconnect() { + mLogger.log("Disconnecting from device: " + mDevice); + stopServices(); + unregisterHid(); + + mLogger.log("Disconnected."); + mConnected = false; + mDevice = null; + mAccessoryConnection = null; + mAccessoryInterface = null; + mControlEndpoint = null; + if (mTransport != null) { + mTransport.close(); + mTransport = null; + } + } + + private void registerHid() { + mLogger.log("Registering HID multitouch device."); + + mMultitouch = new UsbHid.Multitouch(MULTITOUCH_REPORT_ID, MULTITOUCH_MAX_CONTACTS, + mSurfaceView.getWidth(), mSurfaceView.getHeight()); + + mHidBuffer.clear(); + mMultitouch.generateDescriptor(mHidBuffer); + mHidBuffer.flip(); + + mLogger.log("HID descriptor size: " + mHidBuffer.limit()); + mLogger.log("HID report size: " + mMultitouch.getReportSize()); + + final int maxPacketSize = mControlEndpoint.getMaxPacketSize(); + mLogger.log("Control endpoint max packet size: " + maxPacketSize); + if (mMultitouch.getReportSize() > maxPacketSize) { + mLogger.logError("HID report is too big for this accessory."); + return; + } + + int len = mAccessoryConnection.controlTransfer( + UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, + UsbAccessoryConstants.ACCESSORY_REGISTER_HID, + MULTITOUCH_DEVICE_ID, mHidBuffer.limit(), null, 0, 10000); + if (len != 0) { + mLogger.logError("Device rejected ACCESSORY_REGISTER_HID request."); + return; + } + + while (mHidBuffer.hasRemaining()) { + int position = mHidBuffer.position(); + int count = Math.min(mHidBuffer.remaining(), maxPacketSize); + len = mAccessoryConnection.controlTransfer( + UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, + UsbAccessoryConstants.ACCESSORY_SET_HID_REPORT_DESC, + MULTITOUCH_DEVICE_ID, 0, + mHidBuffer.array(), position, count, 10000); + if (len != count) { + mLogger.logError("Device rejected ACCESSORY_SET_HID_REPORT_DESC request."); + return; + } + mHidBuffer.position(position + count); + } + + mLogger.log("HID device registered."); + + mMultitouchEnabled = true; + if (mMultitouchContacts == null) { + mMultitouchContacts = new UsbHid.Multitouch.Contact[MULTITOUCH_MAX_CONTACTS]; + for (int i = 0; i < MULTITOUCH_MAX_CONTACTS; i++) { + mMultitouchContacts[i] = new UsbHid.Multitouch.Contact(); + } + } + } + + private void unregisterHid() { + mMultitouch = null; + mMultitouchContacts = null; + mMultitouchEnabled = false; + } + + private void sendHidTouch(MotionEvent event) { + if (mMultitouchEnabled) { + mLogger.log("Sending touch event: " + event); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: { + final int pointerCount = + Math.min(MULTITOUCH_MAX_CONTACTS, event.getPointerCount()); + final int historySize = event.getHistorySize(); + for (int p = 0; p < pointerCount; p++) { + mMultitouchContacts[p].id = event.getPointerId(p); + } + for (int h = 0; h < historySize; h++) { + for (int p = 0; p < pointerCount; p++) { + mMultitouchContacts[p].x = (int)event.getHistoricalX(p, h); + mMultitouchContacts[p].y = (int)event.getHistoricalY(p, h); + } + sendHidTouchReport(pointerCount); + } + for (int p = 0; p < pointerCount; p++) { + mMultitouchContacts[p].x = (int)event.getX(p); + mMultitouchContacts[p].y = (int)event.getY(p); + } + sendHidTouchReport(pointerCount); + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + sendHidTouchReport(0); + break; + } + } + } + + private void sendHidTouchReport(int contactCount) { + mHidBuffer.clear(); + mMultitouch.generateReport(mHidBuffer, mMultitouchContacts, contactCount); + mHidBuffer.flip(); + + int count = mHidBuffer.limit(); + int len = mAccessoryConnection.controlTransfer( + UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, + UsbAccessoryConstants.ACCESSORY_SEND_HID_EVENT, + MULTITOUCH_DEVICE_ID, 0, + mHidBuffer.array(), 0, count, 10000); + if (len != count) { + mLogger.logError("Device rejected ACCESSORY_SEND_HID_EVENT request."); + return; + } + } + + private void startServices() { + mDisplaySinkService = new DisplaySinkService(this, mTransport, + getResources().getConfiguration().densityDpi); + mDisplaySinkService.start(); + + if (mAttached) { + mDisplaySinkService.setSurfaceView(mSurfaceView); + } + } + + private void stopServices() { + if (mDisplaySinkService != null) { + mDisplaySinkService.stop(); + mDisplaySinkService = null; + } + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + mAttached = true; + if (mDisplaySinkService != null) { + mDisplaySinkService.setSurfaceView(mSurfaceView); + } + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mAttached = false; + if (mDisplaySinkService != null) { + mDisplaySinkService.setSurfaceView(null); + } + } + + private int getProtocol(UsbDeviceConnection conn) { + byte buffer[] = new byte[2]; + int len = conn.controlTransfer( + UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_VENDOR, + UsbAccessoryConstants.ACCESSORY_GET_PROTOCOL, 0, 0, buffer, 2, 10000); + if (len != 2) { + return -1; + } + return (buffer[1] << 8) | buffer[0]; + } + + private void sendString(UsbDeviceConnection conn, int index, String string) { + byte[] buffer = (string + "\0").getBytes(); + int len = conn.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, + UsbAccessoryConstants.ACCESSORY_SEND_STRING, 0, index, + buffer, buffer.length, 10000); + if (len != buffer.length) { + mLogger.logError("Failed to send string " + index + ": \"" + string + "\""); + } else { + mLogger.log("Sent string " + index + ": \"" + string + "\""); + } + } + + private static boolean isAccessory(UsbDevice device) { + final int vid = device.getVendorId(); + final int pid = device.getProductId(); + return vid == UsbAccessoryConstants.USB_ACCESSORY_VENDOR_ID + && (pid == UsbAccessoryConstants.USB_ACCESSORY_PRODUCT_ID + || pid == UsbAccessoryConstants.USB_ACCESSORY_ADB_PRODUCT_ID); + } + + class TextLogger extends Logger { + @Override + public void log(final String message) { + Log.d(TAG, message); + + mLogTextView.post(new Runnable() { + @Override + public void run() { + mLogTextView.append(message); + mLogTextView.append("\n"); + } + }); + } + } + + class DeviceReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (device != null) { + String action = intent.getAction(); + if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) { + onDeviceAttached(device); + } else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) { + onDeviceDetached(device); + } else if (action.equals(ACTION_USB_DEVICE_PERMISSION)) { + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + mLogger.log("Device permission granted: " + device); + onDeviceAttached(device); + } else { + mLogger.logError("Device permission denied: " + device); + } + } + } + } + } +} diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryBulkTransport.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryBulkTransport.java new file mode 100644 index 0000000..a15bfad --- /dev/null +++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryBulkTransport.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.sink; + +import com.android.accessorydisplay.common.Logger; +import com.android.accessorydisplay.common.Transport; + +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; + +import java.io.IOException; + +/** + * Sends or receives messages using bulk endpoints associated with a {@link UsbDevice} + * that represents a USB accessory. + */ +public class UsbAccessoryBulkTransport extends Transport { + private static final int TIMEOUT_MILLIS = 1000; + + private UsbDeviceConnection mConnection; + private UsbEndpoint mBulkInEndpoint; + private UsbEndpoint mBulkOutEndpoint; + + public UsbAccessoryBulkTransport(Logger logger, UsbDeviceConnection connection, + UsbEndpoint bulkInEndpoint, UsbEndpoint bulkOutEndpoint) { + super(logger, 16384); + mConnection = connection; + mBulkInEndpoint = bulkInEndpoint; + mBulkOutEndpoint = bulkOutEndpoint; + } + + @Override + protected void ioClose() { + mConnection = null; + mBulkInEndpoint = null; + mBulkOutEndpoint = null; + } + + @Override + protected int ioRead(byte[] buffer, int offset, int count) throws IOException { + if (mConnection == null) { + throw new IOException("Connection was closed."); + } + return mConnection.bulkTransfer(mBulkInEndpoint, buffer, offset, count, -1); + } + + @Override + protected void ioWrite(byte[] buffer, int offset, int count) throws IOException { + if (mConnection == null) { + throw new IOException("Connection was closed."); + } + int result = mConnection.bulkTransfer(mBulkOutEndpoint, + buffer, offset, count, TIMEOUT_MILLIS); + if (result < 0) { + throw new IOException("Bulk transfer failed."); + } + } +} diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryConstants.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryConstants.java new file mode 100644 index 0000000..8197d6b --- /dev/null +++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbAccessoryConstants.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.sink; + +// Constants from kernel include/linux/usb/f_accessory.h +final class UsbAccessoryConstants { + /* Use Google Vendor ID when in accessory mode */ + public static final int USB_ACCESSORY_VENDOR_ID = 0x18D1; + + /* Product ID to use when in accessory mode */ + public static final int USB_ACCESSORY_PRODUCT_ID = 0x2D00; + + /* Product ID to use when in accessory mode and adb is enabled */ + public static final int USB_ACCESSORY_ADB_PRODUCT_ID = 0x2D01; + + /* Indexes for strings sent by the host via ACCESSORY_SEND_STRING */ + public static final int ACCESSORY_STRING_MANUFACTURER = 0; + public static final int ACCESSORY_STRING_MODEL = 1; + public static final int ACCESSORY_STRING_DESCRIPTION = 2; + public static final int ACCESSORY_STRING_VERSION = 3; + public static final int ACCESSORY_STRING_URI = 4; + public static final int ACCESSORY_STRING_SERIAL = 5; + + /* Control request for retrieving device's protocol version + * + * requestType: USB_DIR_IN | USB_TYPE_VENDOR + * request: ACCESSORY_GET_PROTOCOL + * value: 0 + * index: 0 + * data version number (16 bits little endian) + * 1 for original accessory support + * 2 adds HID and device to host audio support + */ + public static final int ACCESSORY_GET_PROTOCOL = 51; + + /* Control request for host to send a string to the device + * + * requestType: USB_DIR_OUT | USB_TYPE_VENDOR + * request: ACCESSORY_SEND_STRING + * value: 0 + * index: string ID + * data zero terminated UTF8 string + * + * The device can later retrieve these strings via the + * ACCESSORY_GET_STRING_* ioctls + */ + public static final int ACCESSORY_SEND_STRING = 52; + + /* Control request for starting device in accessory mode. + * The host sends this after setting all its strings to the device. + * + * requestType: USB_DIR_OUT | USB_TYPE_VENDOR + * request: ACCESSORY_START + * value: 0 + * index: 0 + * data none + */ + public static final int ACCESSORY_START = 53; + + /* Control request for registering a HID device. + * Upon registering, a unique ID is sent by the accessory in the + * value parameter. This ID will be used for future commands for + * the device + * + * requestType: USB_DIR_OUT | USB_TYPE_VENDOR + * request: ACCESSORY_REGISTER_HID_DEVICE + * value: Accessory assigned ID for the HID device + * index: total length of the HID report descriptor + * data none + */ + public static final int ACCESSORY_REGISTER_HID = 54; + + /* Control request for unregistering a HID device. + * + * requestType: USB_DIR_OUT | USB_TYPE_VENDOR + * request: ACCESSORY_REGISTER_HID + * value: Accessory assigned ID for the HID device + * index: 0 + * data none + */ + public static final int ACCESSORY_UNREGISTER_HID = 55; + + /* Control request for sending the HID report descriptor. + * If the HID descriptor is longer than the endpoint zero max packet size, + * the descriptor will be sent in multiple ACCESSORY_SET_HID_REPORT_DESC + * commands. The data for the descriptor must be sent sequentially + * if multiple packets are needed. + * + * requestType: USB_DIR_OUT | USB_TYPE_VENDOR + * request: ACCESSORY_SET_HID_REPORT_DESC + * value: Accessory assigned ID for the HID device + * index: offset of data in descriptor + * (needed when HID descriptor is too big for one packet) + * data the HID report descriptor + */ + public static final int ACCESSORY_SET_HID_REPORT_DESC = 56; + + /* Control request for sending HID events. + * + * requestType: USB_DIR_OUT | USB_TYPE_VENDOR + * request: ACCESSORY_SEND_HID_EVENT + * value: Accessory assigned ID for the HID device + * index: 0 + * data the HID report for the event + */ + public static final int ACCESSORY_SEND_HID_EVENT = 57; + + /* Control request for setting the audio mode. + * + * requestType: USB_DIR_OUT | USB_TYPE_VENDOR + * request: ACCESSORY_SET_AUDIO_MODE + * value: 0 - no audio + * 1 - device to host, 44100 16-bit stereo PCM + * index: 0 + * data none + */ + public static final int ACCESSORY_SET_AUDIO_MODE = 58; + + private UsbAccessoryConstants() { + } +} diff --git a/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbHid.java b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbHid.java new file mode 100644 index 0000000..b4fa1fd --- /dev/null +++ b/tests/AccessoryDisplay/sink/src/com/android/accessorydisplay/sink/UsbHid.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.sink; + +import java.nio.ByteBuffer; + +/** + * Helper for creating USB HID descriptors and reports. + */ +final class UsbHid { + private UsbHid() { + } + + /** + * Generates basic Windows 7 compatible HID multitouch descriptors and reports + * that should be supported by recent versions of the Linux hid-multitouch driver. + */ + public static final class Multitouch { + private final int mReportId; + private final int mMaxContacts; + private final int mWidth; + private final int mHeight; + + public Multitouch(int reportId, int maxContacts, int width, int height) { + mReportId = reportId; + mMaxContacts = maxContacts; + mWidth = width; + mHeight = height; + } + + public void generateDescriptor(ByteBuffer buffer) { + buffer.put(new byte[] { + 0x05, 0x0d, // USAGE_PAGE (Digitizers) + 0x09, 0x04, // USAGE (Touch Screen) + (byte)0xa1, 0x01, // COLLECTION (Application) + (byte)0x85, (byte)mReportId, // REPORT_ID (Touch) + 0x09, 0x22, // USAGE (Finger) + (byte)0xa1, 0x00, // COLLECTION (Physical) + 0x09, 0x55, // USAGE (Contact Count Maximum) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, (byte)mMaxContacts, // LOGICAL_MAXIMUM (...) + 0x75, 0x08, // REPORT_SIZE (8) + (byte)0x95, 0x01, // REPORT_COUNT (1) + (byte)0xb1, (byte)mMaxContacts, // FEATURE (Data,Var,Abs) + 0x09, 0x54, // USAGE (Contact Count) + (byte)0x81, 0x02, // INPUT (Data,Var,Abs) + }); + byte maxXLsb = (byte)(mWidth - 1); + byte maxXMsb = (byte)((mWidth - 1) >> 8); + byte maxYLsb = (byte)(mHeight - 1); + byte maxYMsb = (byte)((mHeight - 1) >> 8); + byte[] collection = new byte[] { + 0x05, 0x0d, // USAGE_PAGE (Digitizers) + 0x09, 0x22, // USAGE (Finger) + (byte)0xa1, 0x02, // COLLECTION (Logical) + 0x09, 0x42, // USAGE (Tip Switch) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x75, 0x01, // REPORT_SIZE (1) + (byte)0x81, 0x02, // INPUT (Data,Var,Abs) + 0x09, 0x32, // USAGE (In Range) + (byte)0x81, 0x02, // INPUT (Data,Var,Abs) + 0x09, 0x51, // USAGE (Contact Identifier) + 0x25, 0x3f, // LOGICAL_MAXIMUM (63) + 0x75, 0x06, // REPORT_SIZE (6) + (byte)0x81, 0x02, // INPUT (Data,Var,Abs) + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x30, // USAGE (X) + 0x26, maxXLsb, maxXMsb, // LOGICAL_MAXIMUM (...) + 0x75, 0x10, // REPORT_SIZE (16) + (byte)0x81, 0x02, // INPUT (Data,Var,Abs) + 0x09, 0x31, // USAGE (Y) + 0x26, maxYLsb, maxYMsb, // LOGICAL_MAXIMUM (...) + (byte)0x81, 0x02, // INPUT (Data,Var,Abs) + (byte)0xc0, // END_COLLECTION + }; + for (int i = 0; i < mMaxContacts; i++) { + buffer.put(collection); + } + buffer.put(new byte[] { + (byte)0xc0, // END_COLLECTION + (byte)0xc0, // END_COLLECTION + }); + } + + public void generateReport(ByteBuffer buffer, Contact[] contacts, int contactCount) { + // Report Id + buffer.put((byte)mReportId); + // Contact Count + buffer.put((byte)contactCount); + + for (int i = 0; i < contactCount; i++) { + final Contact contact = contacts[i]; + // Tip Switch, In Range, Contact Identifier + buffer.put((byte)((contact.id << 2) | 0x03)); + // X + buffer.put((byte)contact.x).put((byte)(contact.x >> 8)); + // Y + buffer.put((byte)contact.y).put((byte)(contact.y >> 8)); + } + for (int i = contactCount; i < mMaxContacts; i++) { + buffer.put((byte)0).put((byte)0).put((byte)0).put((byte)0).put((byte)0); + } + } + + public int getReportSize() { + return 2 + mMaxContacts * 5; + } + + public static final class Contact { + public int id; // range 0..63 + public int x; + public int y; + } + } +} diff --git a/tests/AccessoryDisplay/source/Android.mk b/tests/AccessoryDisplay/source/Android.mk new file mode 100644 index 0000000..5d1085d --- /dev/null +++ b/tests/AccessoryDisplay/source/Android.mk @@ -0,0 +1,25 @@ +# Copyright (C) 2013 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) + +# Build the application. +include $(CLEAR_VARS) +LOCAL_PACKAGE_NAME := AccessoryDisplaySource +LOCAL_MODULE_TAGS := tests +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_RESOURCE_DIR = $(LOCAL_PATH)/res +LOCAL_STATIC_JAVA_LIBRARIES := AccessoryDisplayCommon +include $(BUILD_PACKAGE) diff --git a/tests/AccessoryDisplay/source/AndroidManifest.xml b/tests/AccessoryDisplay/source/AndroidManifest.xml new file mode 100644 index 0000000..d3edcb8 --- /dev/null +++ b/tests/AccessoryDisplay/source/AndroidManifest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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.accessorydisplay.source" > + + <uses-feature android:name="android.hardware.usb.accessory"/> + <uses-sdk android:minSdkVersion="18" /> + + <application android:label="@string/app_name" + android:icon="@drawable/ic_app" + android:hardwareAccelerated="true"> + + <activity android:name=".SourceActivity" + android:label="@string/app_name" + android:theme="@android:style/Theme.Holo"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"/> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" + android:resource="@xml/usb_accessory_filter"/> + </activity> + + </application> +</manifest> diff --git a/tests/AccessoryDisplay/source/res/drawable-hdpi/ic_app.png b/tests/AccessoryDisplay/source/res/drawable-hdpi/ic_app.png Binary files differnew file mode 100755 index 0000000..66a1984 --- /dev/null +++ b/tests/AccessoryDisplay/source/res/drawable-hdpi/ic_app.png diff --git a/tests/AccessoryDisplay/source/res/drawable-mdpi/ic_app.png b/tests/AccessoryDisplay/source/res/drawable-mdpi/ic_app.png Binary files differnew file mode 100644 index 0000000..5ae7701 --- /dev/null +++ b/tests/AccessoryDisplay/source/res/drawable-mdpi/ic_app.png diff --git a/tests/AccessoryDisplay/source/res/layout/presentation_content.xml b/tests/AccessoryDisplay/source/res/layout/presentation_content.xml new file mode 100644 index 0000000..bf9566a --- /dev/null +++ b/tests/AccessoryDisplay/source/res/layout/presentation_content.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <android.opengl.GLSurfaceView android:id="@+id/surface_view" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <Button android:id="@+id/explode_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_alignParentBottom="true" + android:text="Explode!" /> +</RelativeLayout> diff --git a/tests/AccessoryDisplay/source/res/layout/source_activity.xml b/tests/AccessoryDisplay/source/res/layout/source_activity.xml new file mode 100644 index 0000000..ff2b818 --- /dev/null +++ b/tests/AccessoryDisplay/source/res/layout/source_activity.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <TextView android:id="@+id/logTextView" + android:layout_width="match_parent" + android:layout_height="match_parent" /> +</LinearLayout> diff --git a/tests/AccessoryDisplay/source/res/values/strings.xml b/tests/AccessoryDisplay/source/res/values/strings.xml new file mode 100644 index 0000000..0b488df --- /dev/null +++ b/tests/AccessoryDisplay/source/res/values/strings.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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="app_name">Accessory Display Source</string> +</resources> diff --git a/tests/AccessoryDisplay/source/res/xml/usb_accessory_filter.xml b/tests/AccessoryDisplay/source/res/xml/usb_accessory_filter.xml new file mode 100644 index 0000000..5313b4e --- /dev/null +++ b/tests/AccessoryDisplay/source/res/xml/usb_accessory_filter.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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> + <!-- Match all devices --> + <usb-accessory manufacturer="Android" model="Accessory Display" /> +</resources> diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/DisplaySourceService.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/DisplaySourceService.java new file mode 100644 index 0000000..ccead44 --- /dev/null +++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/DisplaySourceService.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.source; + +import com.android.accessorydisplay.common.Protocol; +import com.android.accessorydisplay.common.Service; +import com.android.accessorydisplay.common.Transport; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.os.Handler; +import android.os.Message; +import android.view.Display; +import android.view.Surface; + +import java.nio.ByteBuffer; + +public class DisplaySourceService extends Service { + private static final int MSG_DISPATCH_DISPLAY_ADDED = 1; + private static final int MSG_DISPATCH_DISPLAY_REMOVED = 2; + + private static final String DISPLAY_NAME = "Accessory Display"; + private static final int BIT_RATE = 6000000; + private static final int FRAME_RATE = 30; + private static final int I_FRAME_INTERVAL = 10; + + private final Callbacks mCallbacks; + private final ServiceHandler mHandler; + private final DisplayManager mDisplayManager; + + private boolean mSinkAvailable; + private int mSinkWidth; + private int mSinkHeight; + private int mSinkDensityDpi; + + private VirtualDisplayThread mVirtualDisplayThread; + + public DisplaySourceService(Context context, Transport transport, Callbacks callbacks) { + super(context, transport, Protocol.DisplaySourceService.ID); + mCallbacks = callbacks; + mHandler = new ServiceHandler(); + mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); + } + + @Override + public void start() { + super.start(); + + getLogger().log("Sending MSG_QUERY."); + getTransport().sendMessage(Protocol.DisplaySinkService.ID, + Protocol.DisplaySinkService.MSG_QUERY, null); + } + + @Override + public void stop() { + super.stop(); + + handleSinkNotAvailable(); + } + + @Override + public void onMessageReceived(int service, int what, ByteBuffer content) { + switch (what) { + case Protocol.DisplaySourceService.MSG_SINK_AVAILABLE: { + getLogger().log("Received MSG_SINK_AVAILABLE"); + if (content.remaining() >= 12) { + final int width = content.getInt(); + final int height = content.getInt(); + final int densityDpi = content.getInt(); + if (width >= 0 && width <= 4096 + && height >= 0 && height <= 4096 + && densityDpi >= 60 && densityDpi <= 640) { + handleSinkAvailable(width, height, densityDpi); + return; + } + } + getLogger().log("Receive invalid MSG_SINK_AVAILABLE message."); + break; + } + + case Protocol.DisplaySourceService.MSG_SINK_NOT_AVAILABLE: { + getLogger().log("Received MSG_SINK_NOT_AVAILABLE"); + handleSinkNotAvailable(); + break; + } + } + } + + private void handleSinkAvailable(int width, int height, int densityDpi) { + if (mSinkAvailable && mSinkWidth == width && mSinkHeight == height + && mSinkDensityDpi == densityDpi) { + return; + } + + getLogger().log("Accessory display sink available: " + + "width=" + width + ", height=" + height + + ", densityDpi=" + densityDpi); + mSinkAvailable = true; + mSinkWidth = width; + mSinkHeight = height; + mSinkDensityDpi = densityDpi; + createVirtualDisplay(); + } + + private void handleSinkNotAvailable() { + getLogger().log("Accessory display sink not available."); + + mSinkAvailable = false; + mSinkWidth = 0; + mSinkHeight = 0; + mSinkDensityDpi = 0; + releaseVirtualDisplay(); + } + + private void createVirtualDisplay() { + releaseVirtualDisplay(); + + mVirtualDisplayThread = new VirtualDisplayThread( + mSinkWidth, mSinkHeight, mSinkDensityDpi); + mVirtualDisplayThread.start(); + } + + private void releaseVirtualDisplay() { + if (mVirtualDisplayThread != null) { + mVirtualDisplayThread.quit(); + mVirtualDisplayThread = null; + } + } + + public interface Callbacks { + public void onDisplayAdded(Display display); + public void onDisplayRemoved(Display display); + } + + private final class ServiceHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_DISPATCH_DISPLAY_ADDED: { + mCallbacks.onDisplayAdded((Display)msg.obj); + break; + } + + case MSG_DISPATCH_DISPLAY_REMOVED: { + mCallbacks.onDisplayRemoved((Display)msg.obj); + break; + } + } + } + } + + private final class VirtualDisplayThread extends Thread { + private static final int TIMEOUT_USEC = 1000000; + + private final int mWidth; + private final int mHeight; + private final int mDensityDpi; + + private volatile boolean mQuitting; + + public VirtualDisplayThread(int width, int height, int densityDpi) { + mWidth = width; + mHeight = height; + mDensityDpi = densityDpi; + } + + @Override + public void run() { + MediaFormat format = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight); + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, + MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); + format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL); + + MediaCodec codec = MediaCodec.createEncoderByType("video/avc"); + codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + Surface surface = codec.createInputSurface(); + codec.start(); + + VirtualDisplay virtualDisplay = mDisplayManager.createPrivateVirtualDisplay( + DISPLAY_NAME, mWidth, mHeight, mDensityDpi, surface); + if (virtualDisplay != null) { + mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_ADDED, + virtualDisplay.getDisplay()).sendToTarget(); + + stream(codec); + + mHandler.obtainMessage(MSG_DISPATCH_DISPLAY_REMOVED, + virtualDisplay.getDisplay()).sendToTarget(); + virtualDisplay.release(); + } + + codec.signalEndOfInputStream(); + codec.stop(); + } + + public void quit() { + mQuitting = true; + } + + private void stream(MediaCodec codec) { + BufferInfo info = new BufferInfo(); + ByteBuffer[] buffers = null; + while (!mQuitting) { + int index = codec.dequeueOutputBuffer(info, TIMEOUT_USEC); + if (index >= 0) { + if (buffers == null) { + buffers = codec.getOutputBuffers(); + } + + ByteBuffer buffer = buffers[index]; + buffer.limit(info.offset + info.size); + buffer.position(info.offset); + + getTransport().sendMessage(Protocol.DisplaySinkService.ID, + Protocol.DisplaySinkService.MSG_CONTENT, buffer); + codec.releaseOutputBuffer(index, false); + } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + buffers = null; + } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { + getLogger().log("Codec dequeue buffer timed out."); + } + } + } + } +} diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/SourceActivity.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/SourceActivity.java new file mode 100644 index 0000000..c59c958 --- /dev/null +++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/SourceActivity.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.source; + +import com.android.accessorydisplay.common.Logger; +import com.android.accessorydisplay.source.presentation.DemoPresentation; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbAccessory; +import android.hardware.usb.UsbManager; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.text.method.ScrollingMovementMethod; +import android.util.Log; +import android.view.Display; +import android.widget.TextView; + +public class SourceActivity extends Activity { + private static final String TAG = "SourceActivity"; + + private static final String ACTION_USB_ACCESSORY_PERMISSION = + "com.android.accessorydisplay.source.ACTION_USB_ACCESSORY_PERMISSION"; + + private static final String MANUFACTURER = "Android"; + private static final String MODEL = "Accessory Display"; + + private UsbManager mUsbManager; + private AccessoryReceiver mReceiver; + private TextView mLogTextView; + private Logger mLogger; + private Presenter mPresenter; + + private boolean mConnected; + private UsbAccessory mAccessory; + private UsbAccessoryStreamTransport mTransport; + + private DisplaySourceService mDisplaySourceService; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mUsbManager = (UsbManager)getSystemService(Context.USB_SERVICE); + + setContentView(R.layout.source_activity); + + mLogTextView = (TextView) findViewById(R.id.logTextView); + mLogTextView.setMovementMethod(ScrollingMovementMethod.getInstance()); + mLogger = new TextLogger(); + mPresenter = new Presenter(); + + mLogger.log("Waiting for accessory display sink to be attached to USB..."); + + IntentFilter filter = new IntentFilter(); + filter.addAction(UsbManager.ACTION_USB_ACCESSORY_ATTACHED); + filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); + filter.addAction(ACTION_USB_ACCESSORY_PERMISSION); + mReceiver = new AccessoryReceiver(); + registerReceiver(mReceiver, filter); + + Intent intent = getIntent(); + if (intent.getAction().equals(UsbManager.ACTION_USB_ACCESSORY_ATTACHED)) { + UsbAccessory accessory = + (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY); + if (accessory != null) { + onAccessoryAttached(accessory); + } + } else { + UsbAccessory[] accessories = mUsbManager.getAccessoryList(); + if (accessories != null) { + for (UsbAccessory accessory : accessories) { + onAccessoryAttached(accessory); + } + } + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + unregisterReceiver(mReceiver); + } + + @Override + protected void onResume() { + super.onResume(); + + //new DemoPresentation(this, getWindowManager().getDefaultDisplay()).show(); + } + + @Override + protected void onPause() { + super.onPause(); + } + + private void onAccessoryAttached(UsbAccessory accessory) { + mLogger.log("USB accessory attached: " + accessory); + if (!mConnected) { + connect(accessory); + } + } + + private void onAccessoryDetached(UsbAccessory accessory) { + mLogger.log("USB accessory detached: " + accessory); + if (mConnected && accessory.equals(mAccessory)) { + disconnect(); + } + } + + private void connect(UsbAccessory accessory) { + if (!isSink(accessory)) { + mLogger.log("Not connecting to USB accessory because it is not an accessory display sink: " + + accessory); + return; + } + + if (mConnected) { + disconnect(); + } + + // Check whether we have permission to access the accessory. + if (!mUsbManager.hasPermission(accessory)) { + mLogger.log("Prompting the user for access to the accessory."); + Intent intent = new Intent(ACTION_USB_ACCESSORY_PERMISSION); + intent.setPackage(getPackageName()); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + this, 0, intent, PendingIntent.FLAG_ONE_SHOT); + mUsbManager.requestPermission(accessory, pendingIntent); + return; + } + + // Open the accessory. + ParcelFileDescriptor fd = mUsbManager.openAccessory(accessory); + if (fd == null) { + mLogger.logError("Could not obtain accessory connection."); + return; + } + + // All set. + mLogger.log("Connected."); + mConnected = true; + mAccessory = accessory; + mTransport = new UsbAccessoryStreamTransport(mLogger, fd); + startServices(); + mTransport.startReading(); + } + + private void disconnect() { + mLogger.log("Disconnecting from accessory: " + mAccessory); + stopServices(); + + mLogger.log("Disconnected."); + mConnected = false; + mAccessory = null; + if (mTransport != null) { + mTransport.close(); + mTransport = null; + } + } + + private void startServices() { + mDisplaySourceService = new DisplaySourceService(this, mTransport, mPresenter); + mDisplaySourceService.start(); + } + + private void stopServices() { + if (mDisplaySourceService != null) { + mDisplaySourceService.stop(); + mDisplaySourceService = null; + } + } + + private static boolean isSink(UsbAccessory accessory) { + return MANUFACTURER.equals(accessory.getManufacturer()) + && MODEL.equals(accessory.getModel()); + } + + class TextLogger extends Logger { + @Override + public void log(final String message) { + Log.d(TAG, message); + + mLogTextView.post(new Runnable() { + @Override + public void run() { + mLogTextView.append(message); + mLogTextView.append("\n"); + } + }); + } + } + + class AccessoryReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + UsbAccessory accessory = intent.<UsbAccessory>getParcelableExtra( + UsbManager.EXTRA_ACCESSORY); + if (accessory != null) { + String action = intent.getAction(); + if (action.equals(UsbManager.ACTION_USB_ACCESSORY_ATTACHED)) { + onAccessoryAttached(accessory); + } else if (action.equals(UsbManager.ACTION_USB_ACCESSORY_DETACHED)) { + onAccessoryDetached(accessory); + } else if (action.equals(ACTION_USB_ACCESSORY_PERMISSION)) { + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + mLogger.log("Accessory permission granted: " + accessory); + onAccessoryAttached(accessory); + } else { + mLogger.logError("Accessory permission denied: " + accessory); + } + } + } + } + } + + class Presenter implements DisplaySourceService.Callbacks { + private DemoPresentation mPresentation; + + @Override + public void onDisplayAdded(Display display) { + mLogger.log("Accessory display added: " + display); + + mPresentation = new DemoPresentation(SourceActivity.this, display, mLogger); + mPresentation.show(); + } + + @Override + public void onDisplayRemoved(Display display) { + mLogger.log("Accessory display removed: " + display); + + if (mPresentation != null) { + mPresentation.dismiss(); + mPresentation = null; + } + } + } +} diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/UsbAccessoryStreamTransport.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/UsbAccessoryStreamTransport.java new file mode 100644 index 0000000..c28f4359 --- /dev/null +++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/UsbAccessoryStreamTransport.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.source; + +import com.android.accessorydisplay.common.Logger; +import com.android.accessorydisplay.common.Transport; + +import android.hardware.usb.UsbAccessory; +import android.os.ParcelFileDescriptor; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Sends or receives messages over a file descriptor associated with a {@link UsbAccessory}. + */ +public class UsbAccessoryStreamTransport extends Transport { + private ParcelFileDescriptor mFd; + private FileInputStream mInputStream; + private FileOutputStream mOutputStream; + + public UsbAccessoryStreamTransport(Logger logger, ParcelFileDescriptor fd) { + super(logger, 16384); + mFd = fd; + mInputStream = new FileInputStream(fd.getFileDescriptor()); + mOutputStream = new FileOutputStream(fd.getFileDescriptor()); + } + + @Override + protected void ioClose() { + try { + mFd.close(); + } catch (IOException ex) { + } + mFd = null; + mInputStream = null; + mOutputStream = null; + } + + @Override + protected int ioRead(byte[] buffer, int offset, int count) throws IOException { + if (mInputStream == null) { + throw new IOException("Stream was closed."); + } + return mInputStream.read(buffer, offset, count); + } + + @Override + protected void ioWrite(byte[] buffer, int offset, int count) throws IOException { + if (mOutputStream == null) { + throw new IOException("Stream was closed."); + } + mOutputStream.write(buffer, offset, count); + } +} diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/Cube.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/Cube.java new file mode 100644 index 0000000..51d8da9 --- /dev/null +++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/Cube.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.source.presentation; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; + +import javax.microedition.khronos.opengles.GL10; + +/** + * A vertex shaded cube. + */ +class Cube +{ + public Cube() + { + int one = 0x10000; + int vertices[] = { + -one, -one, -one, + one, -one, -one, + one, one, -one, + -one, one, -one, + -one, -one, one, + one, -one, one, + one, one, one, + -one, one, one, + }; + + int colors[] = { + 0, 0, 0, one, + one, 0, 0, one, + one, one, 0, one, + 0, one, 0, one, + 0, 0, one, one, + one, 0, one, one, + one, one, one, one, + 0, one, one, one, + }; + + byte indices[] = { + 0, 4, 5, 0, 5, 1, + 1, 5, 6, 1, 6, 2, + 2, 6, 7, 2, 7, 3, + 3, 7, 4, 3, 4, 0, + 4, 7, 6, 4, 6, 5, + 3, 0, 1, 3, 1, 2 + }; + + // Buffers to be passed to gl*Pointer() functions + // must be direct, i.e., they must be placed on the + // native heap where the garbage collector cannot + // move them. + // + // Buffers with multi-byte datatypes (e.g., short, int, float) + // must have their byte order set to native order + + ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4); + vbb.order(ByteOrder.nativeOrder()); + mVertexBuffer = vbb.asIntBuffer(); + mVertexBuffer.put(vertices); + mVertexBuffer.position(0); + + ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4); + cbb.order(ByteOrder.nativeOrder()); + mColorBuffer = cbb.asIntBuffer(); + mColorBuffer.put(colors); + mColorBuffer.position(0); + + mIndexBuffer = ByteBuffer.allocateDirect(indices.length); + mIndexBuffer.put(indices); + mIndexBuffer.position(0); + } + + public void draw(GL10 gl) + { + gl.glFrontFace(GL10.GL_CW); + gl.glVertexPointer(3, GL10.GL_FIXED, 0, mVertexBuffer); + gl.glColorPointer(4, GL10.GL_FIXED, 0, mColorBuffer); + gl.glDrawElements(GL10.GL_TRIANGLES, 36, GL10.GL_UNSIGNED_BYTE, mIndexBuffer); + } + + private IntBuffer mVertexBuffer; + private IntBuffer mColorBuffer; + private ByteBuffer mIndexBuffer; +} diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/CubeRenderer.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/CubeRenderer.java new file mode 100644 index 0000000..51dc82a --- /dev/null +++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/CubeRenderer.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.source.presentation; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import android.opengl.GLSurfaceView; + +/** + * Render a pair of tumbling cubes. + */ + +public class CubeRenderer implements GLSurfaceView.Renderer { + private boolean mTranslucentBackground; + private Cube mCube; + private float mAngle; + private float mScale = 1.0f; + private boolean mExploding; + + public CubeRenderer(boolean useTranslucentBackground) { + mTranslucentBackground = useTranslucentBackground; + mCube = new Cube(); + } + + public void explode() { + mExploding = true; + } + + public void onDrawFrame(GL10 gl) { + /* + * Usually, the first thing one might want to do is to clear + * the screen. The most efficient way of doing this is to use + * glClear(). + */ + + gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); + + /* + * Now we're ready to draw some 3D objects + */ + + gl.glMatrixMode(GL10.GL_MODELVIEW); + gl.glLoadIdentity(); + gl.glTranslatef(0, 0, -3.0f); + gl.glRotatef(mAngle, 0, 1, 0); + gl.glRotatef(mAngle*0.25f, 1, 0, 0); + + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + gl.glEnableClientState(GL10.GL_COLOR_ARRAY); + + gl.glScalef(mScale, mScale, mScale); + mCube.draw(gl); + + gl.glRotatef(mAngle*2.0f, 0, 1, 1); + gl.glTranslatef(0.5f, 0.5f, 0.5f); + + mCube.draw(gl); + + mAngle += 1.2f; + + if (mExploding) { + mScale *= 1.02f; + if (mScale > 4.0f) { + mScale = 1.0f; + mExploding = false; + } + } + } + + public void onSurfaceChanged(GL10 gl, int width, int height) { + gl.glViewport(0, 0, width, height); + + /* + * Set our projection matrix. This doesn't have to be done + * each time we draw, but usually a new projection needs to + * be set when the viewport is resized. + */ + + float ratio = (float) width / height; + gl.glMatrixMode(GL10.GL_PROJECTION); + gl.glLoadIdentity(); + gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10); + } + + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + /* + * By default, OpenGL enables features that improve quality + * but reduce performance. One might want to tweak that + * especially on software renderer. + */ + gl.glDisable(GL10.GL_DITHER); + + /* + * Some one-time OpenGL initialization can be made here + * probably based on features of this particular context + */ + gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, + GL10.GL_FASTEST); + + if (mTranslucentBackground) { + gl.glClearColor(0,0,0,0); + } else { + gl.glClearColor(1,1,1,1); + } + gl.glEnable(GL10.GL_CULL_FACE); + gl.glShadeModel(GL10.GL_SMOOTH); + gl.glEnable(GL10.GL_DEPTH_TEST); + } +} diff --git a/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/DemoPresentation.java b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/DemoPresentation.java new file mode 100644 index 0000000..517b7fc --- /dev/null +++ b/tests/AccessoryDisplay/source/src/com/android/accessorydisplay/source/presentation/DemoPresentation.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2013 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.accessorydisplay.source.presentation; + +import com.android.accessorydisplay.common.Logger; +import com.android.accessorydisplay.source.R; + +import android.app.Presentation; +import android.content.Context; +import android.content.res.Resources; +import android.opengl.GLSurfaceView; +import android.os.Bundle; +import android.view.Display; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Button; + +/** + * The presentation to show on the accessory display. + * <p> + * Note that this display may have different metrics from the display on which + * the main activity is showing so we must be careful to use the presentation's + * own {@link Context} whenever we load resources. + * </p> + */ +public final class DemoPresentation extends Presentation { + private final Logger mLogger; + + private GLSurfaceView mSurfaceView; + private CubeRenderer mRenderer; + private Button mExplodeButton; + + public DemoPresentation(Context context, Display display, Logger logger) { + super(context, display); + mLogger = logger; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + // Be sure to call the super class. + super.onCreate(savedInstanceState); + + // Get the resources for the context of the presentation. + // Notice that we are getting the resources from the context of the presentation. + Resources r = getContext().getResources(); + + // Inflate the layout. + setContentView(R.layout.presentation_content); + + // Set up the surface view for visual interest. + mRenderer = new CubeRenderer(false); + mSurfaceView = (GLSurfaceView)findViewById(R.id.surface_view); + mSurfaceView.setRenderer(mRenderer); + + // Add a button. + mExplodeButton = (Button)findViewById(R.id.explode_button); + mExplodeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mRenderer.explode(); + } + }); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + mLogger.log("Received touch event: " + event); + return super.onTouchEvent(event); + } +}
\ No newline at end of file diff --git a/wifi/java/android/net/wifi/WifiNative.java b/wifi/java/android/net/wifi/WifiNative.java index 375a160..1fbed3d 100644 --- a/wifi/java/android/net/wifi/WifiNative.java +++ b/wifi/java/android/net/wifi/WifiNative.java @@ -198,7 +198,6 @@ public class WifiNative { /** * Format of results: * ================= - * id=1 * bssid=68:7f:74:d7:1b:6e * freq=2412 * level=-43 @@ -209,11 +208,10 @@ public class WifiNative { * ==== * * RANGE=ALL gets all scan results - * RANGE=ID- gets results from ID * MASK=<N> see wpa_supplicant/src/common/wpa_ctrl.h for details */ - public String scanResults(int sid) { - return doStringCommand("BSS RANGE=" + sid + "- MASK=0x21987"); + public String scanResults() { + return doStringCommand("BSS RANGE=ALL MASK=0x21987"); } public boolean startDriver() { diff --git a/wifi/java/android/net/wifi/WifiStateMachine.java b/wifi/java/android/net/wifi/WifiStateMachine.java index 1040ab1..946bbad 100644 --- a/wifi/java/android/net/wifi/WifiStateMachine.java +++ b/wifi/java/android/net/wifi/WifiStateMachine.java @@ -68,8 +68,8 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.os.WorkSource; import android.provider.Settings; -import android.util.LruCache; import android.text.TextUtils; +import android.util.LruCache; import com.android.internal.R; import com.android.internal.app.IBatteryStats; @@ -1339,7 +1339,6 @@ public class WifiStateMachine extends StateMachine { mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL); } - private static final String ID_STR = "id="; private static final String BSSID_STR = "bssid="; private static final String FREQ_STR = "freq="; private static final String LEVEL_STR = "level="; @@ -1351,8 +1350,6 @@ public class WifiStateMachine extends StateMachine { /** * Format: - * - * id=1 * bssid=68:7f:76:d7:1a:6e * freq=2412 * level=-44 @@ -1360,7 +1357,6 @@ public class WifiStateMachine extends StateMachine { * flags=[WPA2-PSK-CCMP][WPS][ESS] * ssid=zfdy * ==== - * id=2 * bssid=68:5f:74:d7:1a:6f * freq=5180 * level=-73 @@ -1369,43 +1365,16 @@ public class WifiStateMachine extends StateMachine { * ssid=zuby * ==== */ - private void setScanResults() { + private void setScanResults(String scanResults) { String bssid = ""; int level = 0; int freq = 0; long tsf = 0; String flags = ""; WifiSsid wifiSsid = null; - String scanResults; - String tmpResults; - StringBuffer scanResultsBuf = new StringBuffer(); - int sid = 0; - - while (true) { - tmpResults = mWifiNative.scanResults(sid); - if (TextUtils.isEmpty(tmpResults)) break; - scanResultsBuf.append(tmpResults); - scanResultsBuf.append("\n"); - String[] lines = tmpResults.split("\n"); - sid = -1; - for (int i=lines.length - 1; i >= 0; i--) { - if (lines[i].startsWith(END_STR)) { - break; - } else if (lines[i].startsWith(ID_STR)) { - try { - sid = Integer.parseInt(lines[i].substring(ID_STR.length())) + 1; - } catch (NumberFormatException e) { - // Nothing to do - } - break; - } - } - if (sid == -1) break; - } - scanResults = scanResultsBuf.toString(); - if (TextUtils.isEmpty(scanResults)) { - return; + if (scanResults == null) { + return; } synchronized(mScanResultCache) { @@ -2232,7 +2201,7 @@ public class WifiStateMachine extends StateMachine { sendMessageDelayed(CMD_START_SUPPLICANT, SUPPLICANT_RESTART_INTERVAL_MSECS); break; case WifiMonitor.SCAN_RESULTS_EVENT: - setScanResults(); + setScanResults(mWifiNative.scanResults()); sendScanResultsAvailableBroadcast(); mScanResultIsPending = false; break; |
