diff options
60 files changed, 4759 insertions, 537 deletions
@@ -255,10 +255,14 @@ LOCAL_SRC_FILES += \ media/java/android/media/IAudioService.aidl \ media/java/android/media/IAudioFocusDispatcher.aidl \ media/java/android/media/IAudioRoutesObserver.aidl \ + media/java/android/media/IMediaRouterClient.aidl \ + media/java/android/media/IMediaRouterService.aidl \ media/java/android/media/IMediaScannerListener.aidl \ media/java/android/media/IMediaScannerService.aidl \ media/java/android/media/IRemoteControlClient.aidl \ media/java/android/media/IRemoteControlDisplay.aidl \ + media/java/android/media/IRemoteDisplayCallback.aidl \ + media/java/android/media/IRemoteDisplayProvider.aidl \ media/java/android/media/IRemoteVolumeObserver.aidl \ media/java/android/media/IRingtonePlayer.aidl \ telephony/java/com/android/internal/telephony/IPhoneStateListener.aidl \ diff --git a/core/java/android/app/MediaRouteActionProvider.java b/core/java/android/app/MediaRouteActionProvider.java index 63b641c..6839c8e 100644 --- a/core/java/android/app/MediaRouteActionProvider.java +++ b/core/java/android/app/MediaRouteActionProvider.java @@ -60,7 +60,7 @@ public class MediaRouteActionProvider extends ActionProvider { } mRouteTypes = types; if (types != 0) { - mRouter.addCallback(types, mCallback); + mRouter.addCallback(types, mCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); } if (mView != null) { mView.setRouteTypes(mRouteTypes); diff --git a/core/java/android/app/MediaRouteButton.java b/core/java/android/app/MediaRouteButton.java index 2464e35..e75dc29 100644 --- a/core/java/android/app/MediaRouteButton.java +++ b/core/java/android/app/MediaRouteButton.java @@ -128,14 +128,14 @@ public class MediaRouteButton extends View { if (mToggleMode) { if (mRemoteActive) { - mRouter.selectRouteInt(mRouteTypes, mRouter.getDefaultRoute()); + mRouter.selectRouteInt(mRouteTypes, mRouter.getDefaultRoute(), true); } else { final int N = mRouter.getRouteCount(); for (int i = 0; i < N; i++) { final RouteInfo route = mRouter.getRouteAt(i); if ((route.getSupportedTypes() & mRouteTypes) != 0 && route != mRouter.getDefaultRoute()) { - mRouter.selectRouteInt(mRouteTypes, route); + mRouter.selectRouteInt(mRouteTypes, route, true); } } } @@ -206,7 +206,8 @@ public class MediaRouteButton extends View { if (mAttachedToWindow) { updateRouteInfo(); - mRouter.addCallback(types, mRouterCallback); + mRouter.addCallback(types, mRouterCallback, + MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); } } @@ -222,8 +223,7 @@ public class MediaRouteButton extends View { void updateRemoteIndicator() { final RouteInfo selected = mRouter.getSelectedRoute(mRouteTypes); final boolean isRemote = selected != mRouter.getDefaultRoute(); - final boolean isConnecting = selected != null && - selected.getStatusCode() == RouteInfo.STATUS_CONNECTING; + final boolean isConnecting = selected != null && selected.isConnecting(); boolean needsRefresh = false; if (mRemoteActive != isRemote) { @@ -243,7 +243,7 @@ public class MediaRouteButton extends View { void updateRouteCount() { final int N = mRouter.getRouteCount(); int count = 0; - boolean hasVideoRoutes = false; + boolean scanRequired = false; for (int i = 0; i < N; i++) { final RouteInfo route = mRouter.getRouteAt(i); final int routeTypes = route.getSupportedTypes(); @@ -253,8 +253,9 @@ public class MediaRouteButton extends View { } else { count++; } - if ((routeTypes & MediaRouter.ROUTE_TYPE_LIVE_VIDEO) != 0) { - hasVideoRoutes = true; + if (((routeTypes & MediaRouter.ROUTE_TYPE_LIVE_VIDEO + | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) != 0) { + scanRequired = true; } } } @@ -262,9 +263,10 @@ public class MediaRouteButton extends View { setEnabled(count != 0); // Only allow toggling if we have more than just user routes. - // Don't toggle if we support video routes, we may have to let the dialog scan. - mToggleMode = count == 2 && (mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_AUDIO) != 0 && - !hasVideoRoutes; + // Don't toggle if we support video or remote display routes, we may have to + // let the dialog scan. + mToggleMode = count == 2 && (mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_AUDIO) != 0 + && !scanRequired; } @Override @@ -318,7 +320,8 @@ public class MediaRouteButton extends View { super.onAttachedToWindow(); mAttachedToWindow = true; if (mRouteTypes != 0) { - mRouter.addCallback(mRouteTypes, mRouterCallback); + mRouter.addCallback(mRouteTypes, mRouterCallback, + MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); updateRouteInfo(); } } diff --git a/core/java/android/hardware/camera2/impl/CameraDevice.java b/core/java/android/hardware/camera2/impl/CameraDevice.java index c428a17..814aa96 100644 --- a/core/java/android/hardware/camera2/impl/CameraDevice.java +++ b/core/java/android/hardware/camera2/impl/CameraDevice.java @@ -279,6 +279,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { checkIfCameraClosed(); int requestId; + if (repeating) { + stopRepeating(); + } + try { requestId = mRemoteDevice.submitRequest(request, repeating); } catch (CameraRuntimeException e) { @@ -293,10 +297,6 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } if (repeating) { - // Queue for deletion after in-flight requests finish - if (mRepeatingRequestId != REQUEST_ID_NONE) { - mRepeatingRequestIdDeletedList.add(mRepeatingRequestId); - } mRepeatingRequestId = requestId; } diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 27e3b08..d3f63b4 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -645,6 +645,15 @@ public final class Display { || uid == 0; } + /** + * Returns true if the display is a public presentation display. + * @hide + */ + public boolean isPublicPresentation() { + return (mFlags & (Display.FLAG_PRIVATE | Display.FLAG_PRESENTATION)) == + Display.FLAG_PRESENTATION; + } + private void updateDisplayInfoLocked() { // Note: The display manager caches display info objects on our behalf. DisplayInfo newInfo = mGlobal.getDisplayInfo(mDisplayId); diff --git a/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java b/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java index e300021..268dcf6 100644 --- a/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java +++ b/core/java/com/android/internal/app/MediaRouteChooserDialogFragment.java @@ -501,7 +501,7 @@ public class MediaRouteChooserDialogFragment extends DialogFragment { final RouteInfo route = (RouteInfo) item; if (type == VIEW_ROUTE) { - mRouter.selectRouteInt(mRouteTypes, route); + mRouter.selectRouteInt(mRouteTypes, route, true); dismiss(); } else if (type == VIEW_GROUPING_ROUTE) { final Checkable c = (Checkable) view; @@ -514,7 +514,7 @@ public class MediaRouteChooserDialogFragment extends DialogFragment { if (mRouter.getSelectedRoute(mRouteTypes) == oldGroup) { // Old group was selected but is now empty. Select the group // we're manipulating since that's where the last route went. - mRouter.selectRouteInt(mRouteTypes, mEditingGroup); + mRouter.selectRouteInt(mRouteTypes, mEditingGroup, true); } oldGroup.removeRoute(route); mEditingGroup.addRoute(route); @@ -555,7 +555,7 @@ public class MediaRouteChooserDialogFragment extends DialogFragment { mEditingGroup = group; mCategoryEditingGroups = group.getCategory(); getDialog().setCanceledOnTouchOutside(false); - mRouter.selectRouteInt(mRouteTypes, mEditingGroup); + mRouter.selectRouteInt(mRouteTypes, mEditingGroup, true); update(); scrollToEditingGroup(); } diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 3381959..4f3b5b3 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -197,10 +197,14 @@ class ZygoteConnection { try { parsedArgs = new Arguments(args); + if (parsedArgs.permittedCapabilities != 0 || parsedArgs.effectiveCapabilities != 0) { + throw new ZygoteSecurityException("Client may not specify capabilities: " + + "permitted=0x" + Long.toHexString(parsedArgs.permittedCapabilities) + + ", effective=0x" + Long.toHexString(parsedArgs.effectiveCapabilities)); + } applyUidSecurityPolicy(parsedArgs, peer, peerSecurityContext); applyRlimitSecurityPolicy(parsedArgs, peer, peerSecurityContext); - applyCapabilitiesSecurityPolicy(parsedArgs, peer, peerSecurityContext); applyInvokeWithSecurityPolicy(parsedArgs, peer, peerSecurityContext); applyseInfoSecurityPolicy(parsedArgs, peer, peerSecurityContext); @@ -703,71 +707,6 @@ class ZygoteConnection { } /** - * Applies zygote security policy per bug #1042973. A root peer may - * spawn an instance with any capabilities. All other uids may spawn - * instances with any of the capabilities in the peer's permitted set - * but no more. - * - * @param args non-null; zygote spawner arguments - * @param peer non-null; peer credentials - * @throws ZygoteSecurityException - */ - private static void applyCapabilitiesSecurityPolicy( - Arguments args, Credentials peer, String peerSecurityContext) - throws ZygoteSecurityException { - - if (args.permittedCapabilities == 0 - && args.effectiveCapabilities == 0) { - // nothing to check - return; - } - - boolean allowed = SELinux.checkSELinuxAccess(peerSecurityContext, - peerSecurityContext, - "zygote", - "specifycapabilities"); - if (!allowed) { - throw new ZygoteSecurityException( - "Peer may not specify capabilities"); - } - - if (peer.getUid() == 0) { - // root may specify anything - return; - } - - long permittedCaps; - - try { - permittedCaps = ZygoteInit.capgetPermitted(peer.getPid()); - } catch (IOException ex) { - throw new ZygoteSecurityException( - "Error retrieving peer's capabilities."); - } - - /* - * Ensure that the client did not specify an effective set larger - * than the permitted set. The kernel will enforce this too, but we - * do it here to make the following check easier. - */ - if (((~args.permittedCapabilities) & args.effectiveCapabilities) != 0) { - throw new ZygoteSecurityException( - "Effective capabilities cannot be superset of " - + " permitted capabilities" ); - } - - /* - * Ensure that the new permitted (and thus the new effective) set is - * a subset of the peer process's permitted set - */ - - if (((~permittedCaps) & args.permittedCapabilities) != 0) { - throw new ZygoteSecurityException( - "Peer specified unpermitted capabilities" ); - } - } - - /** * Applies zygote security policy. * Based on the credentials of the process issuing a zygote command: * <ol> diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index a7faadf..48092f6 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -723,15 +723,6 @@ public class ZygoteInit { throws IOException; /** - * Retrieves the permitted capability set from another process. - * - * @param pid >=0 process ID or 0 for this process - * @throws IOException on error - */ - static native long capgetPermitted(int pid) - throws IOException; - - /** * Invokes select() on the provider array of file descriptors (selecting * for readability only). Array elements of null are ignored. * diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java index d82831f..e339c44 100644 --- a/core/java/com/android/internal/widget/PointerLocationView.java +++ b/core/java/com/android/internal/widget/PointerLocationView.java @@ -31,11 +31,13 @@ import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; +import android.view.WindowManagerPolicy.PointerEventListener; import android.view.MotionEvent.PointerCoords; import java.util.ArrayList; -public class PointerLocationView extends View implements InputDeviceListener { +public class PointerLocationView extends View implements InputDeviceListener, + PointerEventListener { private static final String TAG = "Pointer"; // The system property key used to specify an alternate velocity tracker strategy @@ -520,7 +522,8 @@ public class PointerLocationView extends View implements InputDeviceListener { .toString()); } - public void addPointerEvent(MotionEvent event) { + @Override + public void onPointerEvent(MotionEvent event) { final int action = event.getAction(); int NP = mPointers.size(); @@ -648,7 +651,7 @@ public class PointerLocationView extends View implements InputDeviceListener { @Override public boolean onTouchEvent(MotionEvent event) { - addPointerEvent(event); + onPointerEvent(event); if (event.getAction() == MotionEvent.ACTION_DOWN && !isFocused()) { requestFocus(); @@ -660,7 +663,7 @@ public class PointerLocationView extends View implements InputDeviceListener { public boolean onGenericMotionEvent(MotionEvent event) { final int source = event.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { - addPointerEvent(event); + onPointerEvent(event); } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { logMotionEvent("Joystick", event); } else if ((source & InputDevice.SOURCE_CLASS_POSITION) != 0) { diff --git a/core/jni/Android.mk b/core/jni/Android.mk index 68ea9f3..2e0c28e 100644 --- a/core/jni/Android.mk +++ b/core/jni/Android.mk @@ -4,6 +4,7 @@ include $(CLEAR_VARS) LOCAL_CFLAGS += -DHAVE_CONFIG_H -DKHTML_NO_EXCEPTIONS -DGKWQ_NO_JAVA LOCAL_CFLAGS += -DNO_SUPPORT_JS_BINDING -DQT_NO_WHEELEVENT -DKHTML_NO_XBL LOCAL_CFLAGS += -U__APPLE__ +LOCAL_CFLAGS += -Wno-unused-parameter ifeq ($(TARGET_ARCH), arm) LOCAL_CFLAGS += -DPACKED="__attribute__ ((packed))" diff --git a/core/jni/android/graphics/pdf/PdfDocument.cpp b/core/jni/android/graphics/pdf/PdfDocument.cpp index b57a0fe..6175a8f 100644 --- a/core/jni/android/graphics/pdf/PdfDocument.cpp +++ b/core/jni/android/graphics/pdf/PdfDocument.cpp @@ -17,62 +17,138 @@ #include "jni.h" #include "GraphicsJNI.h" #include <android_runtime/AndroidRuntime.h> +#include <vector> + +#include "CreateJavaOutputStreamAdaptor.h" #include "SkCanvas.h" -#include "SkPDFDevice.h" -#include "SkPDFDocument.h" +#include "SkDocument.h" +#include "SkPicture.h" +#include "SkStream.h" #include "SkRect.h" -#include "SkSize.h" -#include "CreateJavaOutputStreamAdaptor.h" -#include "JNIHelp.h" namespace android { -#define LOGD(x...) do { Log::Instance()->printf(Log::ELogD, x); } while(0) +struct PageRecord { -static jint nativeCreateDocument(JNIEnv* env, jobject clazz) { - return reinterpret_cast<jint>(new SkPDFDocument()); -} + PageRecord(int width, int height, const SkRect& contentRect) + : mPicture(new SkPicture()), mWidth(width), mHeight(height) { + mContentRect = contentRect; + } -static void nativeFinalize(JNIEnv* env, jobject thiz, jint documentPtr) { - delete reinterpret_cast<SkPDFDocument*>(documentPtr); -} + ~PageRecord() { + mPicture->unref(); + } -static jint nativeCreatePage(JNIEnv* env, jobject thiz, jint pageWidth, jint pageHeight, - jint contentLeft, jint contentTop, jint contentRight, jint contentBottom) { + SkPicture* const mPicture; + const int mWidth; + const int mHeight; + SkRect mContentRect; +}; + +class PdfDocument { +public: + PdfDocument() { + mCurrentPage = NULL; + } + + SkCanvas* startPage(int width, int height, + int contentLeft, int contentTop, int contentRight, int contentBottom) { + assert(mCurrentPage == NULL); + + SkRect contentRect = SkRect::MakeLTRB( + contentLeft, contentTop, contentRight, contentBottom); + PageRecord* page = new PageRecord(width, height, contentRect); + mPages.push_back(page); + mCurrentPage = page; + + SkCanvas* canvas = page->mPicture->beginRecording( + contentRect.width(), contentRect.height(), 0); + + // We pass this canvas to Java where it is used to construct + // a Java Canvas object which dereferences the pointer when it + // is destroyed, so we have to bump up the reference count. + canvas->ref(); + + return canvas; + } - SkMatrix transformation; - transformation.setTranslate(contentLeft, contentTop); + void finishPage() { + assert(mCurrentPage != NULL); + mCurrentPage->mPicture->endRecording(); + mCurrentPage = NULL; + } - SkISize skPageSize = SkISize::Make(pageWidth, pageHeight); - SkISize skContentSize = SkISize::Make(contentRight - contentLeft, contentBottom - contentTop); + void write(SkWStream* stream) { + SkDocument* document = SkDocument::CreatePDF(stream); + for (unsigned i = 0; i < mPages.size(); i++) { + PageRecord* page = mPages[i]; - SkPDFDevice* skPdfDevice = new SkPDFDevice(skPageSize, skContentSize, transformation); - return reinterpret_cast<jint>(new SkCanvas(skPdfDevice)); + SkCanvas* canvas = document->beginPage(page->mWidth, page->mHeight, + &(page->mContentRect)); + + canvas->clipRect(page->mContentRect); + canvas->translate(page->mContentRect.left(), page->mContentRect.top()); + canvas->drawPicture(*page->mPicture); + + document->endPage(); + } + document->close(); + } + + void close() { + for (unsigned i = 0; i < mPages.size(); i++) { + delete mPages[i]; + } + delete mCurrentPage; + mCurrentPage = NULL; + } + +private: + ~PdfDocument() { + close(); + } + + std::vector<PageRecord*> mPages; + PageRecord* mCurrentPage; +}; + +static jint nativeCreateDocument(JNIEnv* env, jobject thiz) { + return reinterpret_cast<jint>(new PdfDocument()); +} + +static jint nativeStartPage(JNIEnv* env, jobject thiz, jint documentPtr, + jint pageWidth, jint pageHeight, + jint contentLeft, jint contentTop, jint contentRight, jint contentBottom) { + PdfDocument* document = reinterpret_cast<PdfDocument*>(documentPtr); + return reinterpret_cast<jint>(document->startPage(pageWidth, pageHeight, + contentLeft, contentTop, contentRight, contentBottom)); } -static void nativeAppendPage(JNIEnv* env, jobject thiz, jint documentPtr, jint pagePtr) { - SkCanvas* page = reinterpret_cast<SkCanvas*>(pagePtr); - SkPDFDocument* document = reinterpret_cast<SkPDFDocument*>(documentPtr); - SkPDFDevice* device = static_cast<SkPDFDevice*>(page->getDevice()); - document->appendPage(device); +static void nativeFinishPage(JNIEnv* env, jobject thiz, jint documentPtr) { + PdfDocument* document = reinterpret_cast<PdfDocument*>(documentPtr); + document->finishPage(); } -static void nativeWriteTo(JNIEnv* env, jobject clazz, jint documentPtr, - jobject out, jbyteArray chunk) { +static void nativeWriteTo(JNIEnv* env, jobject thiz, jint documentPtr, jobject out, + jbyteArray chunk) { + PdfDocument* document = reinterpret_cast<PdfDocument*>(documentPtr); SkWStream* skWStream = CreateJavaOutputStreamAdaptor(env, out, chunk); - SkPDFDocument* document = reinterpret_cast<SkPDFDocument*>(documentPtr); - document->emitPDF(skWStream); + document->write(skWStream); delete skWStream; } +static void nativeClose(JNIEnv* env, jobject thiz, jint documentPtr) { + PdfDocument* document = reinterpret_cast<PdfDocument*>(documentPtr); + document->close(); +} + static JNINativeMethod gPdfDocument_Methods[] = { {"nativeCreateDocument", "()I", (void*) nativeCreateDocument}, - {"nativeFinalize", "(I)V", (void*) nativeFinalize}, - {"nativeCreatePage", "(IIIIII)I", - (void*) nativeCreatePage}, - {"nativeAppendPage", "(II)V", (void*) nativeAppendPage}, - {"nativeWriteTo", "(ILjava/io/OutputStream;[B)V", (void*) nativeWriteTo} + {"nativeStartPage", "(IIIIIII)I", (void*) nativeStartPage}, + {"nativeFinishPage", "(I)V", (void*) nativeFinishPage}, + {"nativeWriteTo", "(ILjava/io/OutputStream;[B)V", (void*) nativeWriteTo}, + {"nativeClose", "(I)V", (void*) nativeClose} }; int register_android_graphics_pdf_PdfDocument(JNIEnv* env) { diff --git a/core/jni/com_android_internal_os_ZygoteInit.cpp b/core/jni/com_android_internal_os_ZygoteInit.cpp index 44452f0..2233ee3 100644 --- a/core/jni/com_android_internal_os_ZygoteInit.cpp +++ b/core/jni/com_android_internal_os_ZygoteInit.cpp @@ -159,29 +159,6 @@ static void com_android_internal_os_ZygoteInit_setCloseOnExec (JNIEnv *env, } } -static jlong com_android_internal_os_ZygoteInit_capgetPermitted (JNIEnv *env, - jobject clazz, jint pid) -{ - struct __user_cap_header_struct capheader; - struct __user_cap_data_struct capdata; - int err; - - memset (&capheader, 0, sizeof(capheader)); - memset (&capdata, 0, sizeof(capdata)); - - capheader.version = _LINUX_CAPABILITY_VERSION; - capheader.pid = pid; - - err = capget (&capheader, &capdata); - - if (err < 0) { - jniThrowIOException(env, errno); - return 0; - } - - return (jlong) capdata.permitted; -} - static jint com_android_internal_os_ZygoteInit_selectReadable ( JNIEnv *env, jobject clazz, jobjectArray fds) { @@ -274,8 +251,6 @@ static JNINativeMethod gMethods[] = { (void *) com_android_internal_os_ZygoteInit_reopenStdio}, { "setCloseOnExec", "(Ljava/io/FileDescriptor;Z)V", (void *) com_android_internal_os_ZygoteInit_setCloseOnExec}, - { "capgetPermitted", "(I)J", - (void *) com_android_internal_os_ZygoteInit_capgetPermitted }, { "selectReadable", "([Ljava/io/FileDescriptor;)I", (void *) com_android_internal_os_ZygoteInit_selectReadable }, { "createFileDescriptor", "(I)Ljava/io/FileDescriptor;", diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index f0fc9e3..8f15471 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1992,6 +1992,14 @@ android:description="@string/permdesc_bindWallpaper" android:protectionLevel="signature|system" /> + <!-- Must be required by a {@link com.android.media.remotedisplay.RemoteDisplayProvider}, + to ensure that only the system can bind to it. + @hide --> + <permission android:name="android.permission.BIND_REMOTE_DISPLAY" + android:label="@string/permlab_bindRemoteDisplay" + android:description="@string/permdesc_bindRemoteDisplay" + android:protectionLevel="signature" /> + <!-- Must be required by device administration receiver, to ensure that only the system can interact with it. --> <permission android:name="android.permission.BIND_DEVICE_ADMIN" diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index a554407..d219ec1 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1052,6 +1052,12 @@ interface of a wallpaper. Should never be needed for normal apps.</string> <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permlab_bindRemoteDisplay">bind to a remote display</string> + <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> + <string name="permdesc_bindRemoteDisplay">Allows the holder to bind to the top-level + interface of a remote display. Should never be needed for normal apps.</string> + + <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permlab_bindRemoteViews">bind to a widget service</string> <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. --> <string name="permdesc_bindRemoteViews">Allows the holder to bind to the top-level diff --git a/core/tests/bandwidthtests/src/com/android/bandwidthtest/BandwidthTest.java b/core/tests/bandwidthtests/src/com/android/bandwidthtest/BandwidthTest.java index 76b702e..4a58f88 100644 --- a/core/tests/bandwidthtests/src/com/android/bandwidthtest/BandwidthTest.java +++ b/core/tests/bandwidthtests/src/com/android/bandwidthtest/BandwidthTest.java @@ -36,8 +36,6 @@ import com.android.bandwidthtest.util.BandwidthTestUtil; import com.android.bandwidthtest.util.ConnectionUtil; import java.io.File; -import java.util.HashMap; -import java.util.Map; /** * Test that downloads files from a test server and reports the bandwidth metrics collected. @@ -131,8 +129,8 @@ public class BandwidthTest extends InstrumentationTestCase { results.putString("device_id", mDeviceId); results.putString("timestamp", ts); results.putInt("size", FILE_SIZE); - AddStatsToResults(PROF_LABEL, prof_stats, results); - AddStatsToResults(PROC_LABEL, proc_stats, results); + addStatsToResults(PROF_LABEL, prof_stats, results, mUid); + addStatsToResults(PROC_LABEL, proc_stats, results, mUid); getInstrumentation().sendStatus(INSTRUMENTATION_IN_PROGRESS, results); // Clean up. @@ -185,8 +183,8 @@ public class BandwidthTest extends InstrumentationTestCase { results.putString("device_id", mDeviceId); results.putString("timestamp", ts); results.putInt("size", FILE_SIZE); - AddStatsToResults(PROF_LABEL, prof_stats, results); - AddStatsToResults(PROC_LABEL, proc_stats, results); + addStatsToResults(PROF_LABEL, prof_stats, results, mUid); + addStatsToResults(PROC_LABEL, proc_stats, results, mUid); getInstrumentation().sendStatus(INSTRUMENTATION_IN_PROGRESS, results); // Clean up. @@ -242,8 +240,9 @@ public class BandwidthTest extends InstrumentationTestCase { results.putString("device_id", mDeviceId); results.putString("timestamp", ts); results.putInt("size", FILE_SIZE); - AddStatsToResults(PROF_LABEL, prof_stats, results); - AddStatsToResults(PROC_LABEL, proc_stats, results); + addStatsToResults(PROF_LABEL, prof_stats, results, mUid); + // remember to use download manager uid for proc stats + addStatsToResults(PROC_LABEL, proc_stats, results, downloadManagerUid); getInstrumentation().sendStatus(INSTRUMENTATION_IN_PROGRESS, results); // Clean up. @@ -302,46 +301,35 @@ public class BandwidthTest extends InstrumentationTestCase { * @param label to attach to this given stats. * @param stats {@link NetworkStats} to add. * @param results {@link Bundle} to be added to. + * @param uid for which to report the results. */ - public void AddStatsToResults(String label, NetworkStats stats, Bundle results){ + public void addStatsToResults(String label, NetworkStats stats, Bundle results, int uid){ if (results == null || results.isEmpty()) { Log.e(LOG_TAG, "Empty bundle provided."); return; } - // Merge stats across all sets. - Map<Integer, Entry> totalStats = new HashMap<Integer, Entry>(); + Entry totalStats = null; for (int i = 0; i < stats.size(); ++i) { Entry statsEntry = stats.getValues(i, null); // We are only interested in the all inclusive stats. if (statsEntry.tag != 0) { continue; } - Entry mapEntry = null; - if (totalStats.containsKey(statsEntry.uid)) { - mapEntry = totalStats.get(statsEntry.uid); - switch (statsEntry.set) { - case NetworkStats.SET_ALL: - mapEntry.rxBytes = statsEntry.rxBytes; - mapEntry.txBytes = statsEntry.txBytes; - break; - case NetworkStats.SET_DEFAULT: - case NetworkStats.SET_FOREGROUND: - mapEntry.rxBytes += statsEntry.rxBytes; - mapEntry.txBytes += statsEntry.txBytes; - break; - default: - Log.w(LOG_TAG, "Invalid state found in NetworkStats."); - } + // skip stats for other uids + if (statsEntry.uid != uid) { + continue; + } + if (totalStats == null || statsEntry.set == NetworkStats.SET_ALL) { + totalStats = statsEntry; } else { - totalStats.put(statsEntry.uid, statsEntry); + totalStats.rxBytes += statsEntry.rxBytes; + totalStats.txBytes += statsEntry.txBytes; } } - // Ouput merged stats to bundle. - for (Entry entry : totalStats.values()) { - results.putInt(label + "uid", entry.uid); - results.putLong(label + "tx", entry.txBytes); - results.putLong(label + "rx", entry.rxBytes); - } + // Output merged stats to bundle. + results.putInt(label + "uid", totalStats.uid); + results.putLong(label + "tx", totalStats.txBytes); + results.putLong(label + "rx", totalStats.rxBytes); } /** diff --git a/data/sounds/AudioPackage10.mk b/data/sounds/AudioPackage10.mk index 783e1f8..ae4bc88 100644 --- a/data/sounds/AudioPackage10.mk +++ b/data/sounds/AudioPackage10.mk @@ -17,19 +17,19 @@ PRODUCT_COPY_FILES += \ $(LOCAL_PATH)/alarms/ogg/Osmium.ogg:system/media/audio/alarms/Osmium.ogg \ $(LOCAL_PATH)/alarms/ogg/Platinum.ogg:system/media/audio/alarms/Platinum.ogg \ $(LOCAL_PATH)/effects/ogg/Effect_Tick_48k.ogg:system/media/audio/ui/Effect_Tick.ogg \ - $(LOCAL_PATH)/effects/ogg/KeypressStandard_120_48k.ogg:system/media/audio/ui/KeypressStandard.ogg \ - $(LOCAL_PATH)/effects/ogg/KeypressSpacebar_120_48k.ogg:system/media/audio/ui/KeypressSpacebar.ogg \ - $(LOCAL_PATH)/effects/ogg/KeypressDelete_120_48k.ogg:system/media/audio/ui/KeypressDelete.ogg \ - $(LOCAL_PATH)/effects/ogg/KeypressInvalid_120_48k.ogg:system/media/audio/ui/KeypressInvalid.ogg \ - $(LOCAL_PATH)/effects/ogg/KeypressReturn_120_48k.ogg:system/media/audio/ui/KeypressReturn.ogg \ + $(LOCAL_PATH)/effects/ogg/KeypressStandard_48k.ogg:system/media/audio/ui/KeypressStandard.ogg \ + $(LOCAL_PATH)/effects/ogg/KeypressSpacebar_48k.ogg:system/media/audio/ui/KeypressSpacebar.ogg \ + $(LOCAL_PATH)/effects/ogg/KeypressDelete_48k.ogg:system/media/audio/ui/KeypressDelete.ogg \ + $(LOCAL_PATH)/effects/ogg/KeypressInvalid_48k.ogg:system/media/audio/ui/KeypressInvalid.ogg \ + $(LOCAL_PATH)/effects/ogg/KeypressReturn_48k.ogg:system/media/audio/ui/KeypressReturn.ogg \ $(LOCAL_PATH)/effects/ogg/VideoRecord_48k.ogg:system/media/audio/ui/VideoRecord.ogg \ $(LOCAL_PATH)/effects/ogg/camera_click_48k.ogg:system/media/audio/ui/camera_click.ogg \ $(LOCAL_PATH)/effects/ogg/camera_focus.ogg:system/media/audio/ui/camera_focus.ogg \ $(LOCAL_PATH)/effects/ogg/LowBattery.ogg:system/media/audio/ui/LowBattery.ogg \ $(LOCAL_PATH)/effects/ogg/Dock.ogg:system/media/audio/ui/Dock.ogg \ $(LOCAL_PATH)/effects/ogg/Undock.ogg:system/media/audio/ui/Undock.ogg \ - $(LOCAL_PATH)/effects/ogg/Lock.ogg:system/media/audio/ui/Lock.ogg \ - $(LOCAL_PATH)/effects/ogg/Unlock.ogg:system/media/audio/ui/Unlock.ogg \ + $(LOCAL_PATH)/effects/ogg/Lock_48k.ogg:system/media/audio/ui/Lock.ogg \ + $(LOCAL_PATH)/effects/ogg/Unlock_48k.ogg:system/media/audio/ui/Unlock.ogg \ $(LOCAL_PATH)/effects/ogg/WirelessChargingStarted.ogg:system/media/audio/ui/WirelessChargingStarted.ogg \ $(LOCAL_PATH)/notifications/ogg/Adara.ogg:system/media/audio/notifications/Adara.ogg \ $(LOCAL_PATH)/notifications/ogg/Alya.ogg:system/media/audio/notifications/Alya.ogg \ diff --git a/graphics/java/android/graphics/Path.java b/graphics/java/android/graphics/Path.java index 5b04a91..09481d4 100644 --- a/graphics/java/android/graphics/Path.java +++ b/graphics/java/android/graphics/Path.java @@ -16,8 +16,6 @@ package android.graphics; -import android.view.HardwareRenderer; - /** * The Path class encapsulates compound (multiple contour) geometric paths * consisting of straight line segments, quadratic curves, and cubic curves. @@ -39,7 +37,6 @@ public class Path { * @hide */ public Region rects; - private boolean mDetectSimplePaths; private Direction mLastDirection = null; /** @@ -47,7 +44,6 @@ public class Path { */ public Path() { mNativePath = init1(); - mDetectSimplePaths = HardwareRenderer.isAvailable(); } /** @@ -65,7 +61,6 @@ public class Path { } } mNativePath = init2(valNative); - mDetectSimplePaths = HardwareRenderer.isAvailable(); } /** @@ -74,10 +69,8 @@ public class Path { */ public void reset() { isSimplePath = true; - if (mDetectSimplePaths) { - mLastDirection = null; - if (rects != null) rects.setEmpty(); - } + mLastDirection = null; + if (rects != null) rects.setEmpty(); // We promised not to change this, so preserve it around the native // call, which does now reset fill type. final FillType fillType = getFillType(); @@ -91,10 +84,8 @@ public class Path { */ public void rewind() { isSimplePath = true; - if (mDetectSimplePaths) { - mLastDirection = null; - if (rects != null) rects.setEmpty(); - } + mLastDirection = null; + if (rects != null) rects.setEmpty(); native_rewind(mNativePath); } @@ -475,16 +466,14 @@ public class Path { } private void detectSimplePath(float left, float top, float right, float bottom, Direction dir) { - if (mDetectSimplePaths) { - if (mLastDirection == null) { - mLastDirection = dir; - } - if (mLastDirection != dir) { - isSimplePath = false; - } else { - if (rects == null) rects = new Region(); - rects.op((int) left, (int) top, (int) right, (int) bottom, Region.Op.UNION); - } + if (mLastDirection == null) { + mLastDirection = dir; + } + if (mLastDirection != dir) { + isSimplePath = false; + } else { + if (rects == null) rects = new Region(); + rects.op((int) left, (int) top, (int) right, (int) bottom, Region.Op.UNION); } } diff --git a/graphics/java/android/graphics/pdf/PdfDocument.java b/graphics/java/android/graphics/pdf/PdfDocument.java index 81e523d..29d14a2 100644 --- a/graphics/java/android/graphics/pdf/PdfDocument.java +++ b/graphics/java/android/graphics/pdf/PdfDocument.java @@ -18,6 +18,7 @@ package android.graphics.pdf; import android.graphics.Bitmap; import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Rect; import dalvik.system.CloseGuard; @@ -69,6 +70,12 @@ import java.util.List; */ public class PdfDocument { + // TODO: We need a constructor that will take an OutputStream to + // support online data serialization as opposed to the current + // on demand one. The current approach is fine until Skia starts + // to support online PDF generation at which point we need to + // handle this. + private final byte[] mChunk = new byte[4096]; private final CloseGuard mCloseGuard = CloseGuard.get(); @@ -111,7 +118,7 @@ public class PdfDocument { if (pageInfo == null) { throw new IllegalArgumentException("page cannot be null"); } - Canvas canvas = new PdfCanvas(nativeCreatePage(pageInfo.mPageWidth, + Canvas canvas = new PdfCanvas(nativeStartPage(mNativeDocument, pageInfo.mPageWidth, pageInfo.mPageHeight, pageInfo.mContentRect.left, pageInfo.mContentRect.top, pageInfo.mContentRect.right, pageInfo.mContentRect.bottom)); mCurrentPage = new Page(canvas, pageInfo); @@ -142,7 +149,7 @@ public class PdfDocument { } mPages.add(page.getInfo()); mCurrentPage = null; - nativeAppendPage(mNativeDocument, page.mCanvas.mNativeCanvas); + nativeFinishPage(mNativeDocument); page.finish(); } @@ -204,7 +211,7 @@ public class PdfDocument { private void dispose() { if (mNativeDocument != 0) { - nativeFinalize(mNativeDocument); + nativeClose(mNativeDocument); mCloseGuard.close(); mNativeDocument = 0; } @@ -230,14 +237,14 @@ public class PdfDocument { private native int nativeCreateDocument(); - private native void nativeFinalize(int document); + private native void nativeClose(int document); - private native void nativeAppendPage(int document, int page); + private native void nativeFinishPage(int document); private native void nativeWriteTo(int document, OutputStream out, byte[] chunk); - private static native int nativeCreatePage(int pageWidth, int pageHeight, int contentLeft, - int contentTop, int contentRight, int contentBottom); + private static native int nativeStartPage(int documentPtr, int pageWidth, int pageHeight, + int contentLeft, int contentTop, int contentRight, int contentBottom); private final class PdfCanvas extends Canvas { @@ -392,28 +399,28 @@ public class PdfDocument { * Gets the {@link Canvas} of the page. * * <p> - * <strong>Note: </strong> There are some draw operations that are - * not yet supported by the canvas returned by this method. More - * specifically: + * <strong>Note: </strong> There are some draw operations that are not yet + * supported by the canvas returned by this method. More specifically: * <ul> - * <li>{@link Canvas#clipPath(android.graphics.Path) - * Canvas.clipPath(android.graphics.Path)}</li> - * <li>All flavors of {@link Canvas#drawText(String, float, float, - * android.graphics.Paint) Canvas.drawText(String, float, float, - * android.graphics.Paint)}</li> - * <li>All flavors of {@link Canvas#drawPosText(String, float[], - * android.graphics.Paint) Canvas.drawPosText(String, float[], - * android.graphics.Paint)}</li> + * <li>Inverse path clipping performed via {@link Canvas#clipPath(android.graphics.Path, + * android.graphics.Region.Op) Canvas.clipPath(android.graphics.Path, + * android.graphics.Region.Op)} for {@link + * android.graphics.Region.Op#REVERSE_DIFFERENCE + * Region.Op#REVERSE_DIFFERENCE} operations.</li> * <li>{@link Canvas#drawVertices(android.graphics.Canvas.VertexMode, int, * float[], int, float[], int, int[], int, short[], int, int, * android.graphics.Paint) Canvas.drawVertices( * android.graphics.Canvas.VertexMode, int, float[], int, float[], * int, int[], int, short[], int, int, android.graphics.Paint)}</li> - * <li>{@link android.graphics.PorterDuff.Mode#SRC_ATOP PorterDuff.Mode SRC}, + * <li>Color filters set via {@link Paint#setColorFilter( + * android.graphics.ColorFilter)}</li> + * <li>Mask filters set via {@link Paint#setMaskFilter( + * android.graphics.MaskFilter)}</li> + * <li>Some XFER modes such as + * {@link android.graphics.PorterDuff.Mode#SRC_ATOP PorterDuff.Mode SRC}, * {@link android.graphics.PorterDuff.Mode#DST_ATOP PorterDuff.DST_ATOP}, * {@link android.graphics.PorterDuff.Mode#XOR PorterDuff.XOR}, * {@link android.graphics.PorterDuff.Mode#ADD PorterDuff.ADD}</li> - * <li>Perspective transforms</li> * </ul> * * @return The canvas if the page is not finished, null otherwise. diff --git a/media/java/android/media/IMediaRouterClient.aidl b/media/java/android/media/IMediaRouterClient.aidl new file mode 100644 index 0000000..9640dcb --- /dev/null +++ b/media/java/android/media/IMediaRouterClient.aidl @@ -0,0 +1,24 @@ +/* + * 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.media; + +/** + * {@hide} + */ +oneway interface IMediaRouterClient { + void onStateChanged(); +} diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl new file mode 100644 index 0000000..f8f5fdf --- /dev/null +++ b/media/java/android/media/IMediaRouterService.aidl @@ -0,0 +1,35 @@ +/* + * 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.media; + +import android.media.IMediaRouterClient; +import android.media.MediaRouterClientState; + +/** + * {@hide} + */ +interface IMediaRouterService { + void registerClientAsUser(IMediaRouterClient client, String packageName, int userId); + void unregisterClient(IMediaRouterClient client); + + MediaRouterClientState getState(IMediaRouterClient client); + + void setDiscoveryRequest(IMediaRouterClient client, int routeTypes, boolean activeScan); + void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit); + void requestSetVolume(IMediaRouterClient client, String routeId, int volume); + void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction); +} diff --git a/media/java/android/media/IRemoteDisplayCallback.aidl b/media/java/android/media/IRemoteDisplayCallback.aidl new file mode 100644 index 0000000..19cf070 --- /dev/null +++ b/media/java/android/media/IRemoteDisplayCallback.aidl @@ -0,0 +1,26 @@ +/* + * 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.media; + +import android.media.RemoteDisplayState; + +/** + * {@hide} + */ +oneway interface IRemoteDisplayCallback { + void onStateChanged(in RemoteDisplayState state); +} diff --git a/media/java/android/media/IRemoteDisplayProvider.aidl b/media/java/android/media/IRemoteDisplayProvider.aidl new file mode 100644 index 0000000..b0d7379 --- /dev/null +++ b/media/java/android/media/IRemoteDisplayProvider.aidl @@ -0,0 +1,31 @@ +/* + * 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.media; + +import android.media.IRemoteDisplayCallback; + +/** + * {@hide} + */ +oneway interface IRemoteDisplayProvider { + void setCallback(in IRemoteDisplayCallback callback); + void setDiscoveryMode(int mode); + void connect(String id); + void disconnect(String id); + void setVolume(String id, int volume); + void adjustVolume(String id, int delta); +} diff --git a/media/java/android/media/MediaRouter.java b/media/java/android/media/MediaRouter.java index 9a79c94..c184e8f 100644 --- a/media/java/android/media/MediaRouter.java +++ b/media/java/android/media/MediaRouter.java @@ -16,6 +16,8 @@ package android.media; +import com.android.internal.util.Objects; + import android.app.ActivityThread; import android.content.BroadcastReceiver; import android.content.Context; @@ -30,6 +32,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import android.view.Display; @@ -52,14 +55,17 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class MediaRouter { private static final String TAG = "MediaRouter"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); static class Static implements DisplayManager.DisplayListener { // Time between wifi display scans when actively scanning in milliseconds. private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000; + final Context mAppContext; final Resources mResources; final IAudioService mAudioService; final DisplayManager mDisplayService; + final IMediaRouterService mMediaRouterService; final Handler mHandler; final CopyOnWriteArrayList<CallbackInfo> mCallbacks = new CopyOnWriteArrayList<CallbackInfo>(); @@ -79,6 +85,13 @@ public class MediaRouter { WifiDisplayStatus mLastKnownWifiDisplayStatus; boolean mActivelyScanningWifiDisplays; + int mDiscoveryRequestRouteTypes; + boolean mDiscoverRequestActiveScan; + + int mCurrentUserId = -1; + IMediaRouterClient mClient; + MediaRouterClientState mClientState; + final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() { @Override public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) { @@ -101,6 +114,7 @@ public class MediaRouter { }; Static(Context appContext) { + mAppContext = appContext; mResources = Resources.getSystem(); mHandler = new Handler(appContext.getMainLooper()); @@ -109,6 +123,9 @@ public class MediaRouter { mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE); + mMediaRouterService = IMediaRouterService.Stub.asInterface( + ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); + mSystemCategory = new RouteCategory( com.android.internal.R.string.default_audio_route_category_name, ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false); @@ -146,10 +163,13 @@ public class MediaRouter { updateAudioRoutes(newAudioRoutes); } + // Bind to the media router service. + rebindAsUser(UserHandle.myUserId()); + // Select the default route if the above didn't sync us up // appropriately with relevant system state. if (mSelectedRoute == null) { - selectRouteStatic(mDefaultAudioVideo.getSupportedTypes(), mDefaultAudioVideo); + selectDefaultRouteStatic(); } } @@ -197,7 +217,7 @@ public class MediaRouter { dispatchRouteChanged(sStatic.mBluetoothA2dpRoute); } } else if (sStatic.mBluetoothA2dpRoute != null) { - removeRoute(sStatic.mBluetoothA2dpRoute); + removeRouteStatic(sStatic.mBluetoothA2dpRoute); sStatic.mBluetoothA2dpRoute = null; } } @@ -205,16 +225,52 @@ public class MediaRouter { if (mBluetoothA2dpRoute != null) { if (mainType != AudioRoutesInfo.MAIN_SPEAKER && mSelectedRoute == mBluetoothA2dpRoute && !a2dpEnabled) { - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo); + selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo, false); } else if ((mSelectedRoute == mDefaultAudioVideo || mSelectedRoute == null) && a2dpEnabled) { - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute); + selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute, false); } } } - void updateActiveScan() { - if (hasActiveScanCallbackOfType(ROUTE_TYPE_LIVE_VIDEO)) { + void updateDiscoveryRequest() { + // What are we looking for today? + int routeTypes = 0; + int passiveRouteTypes = 0; + boolean activeScan = false; + boolean activeScanWifiDisplay = false; + final int count = mCallbacks.size(); + for (int i = 0; i < count; i++) { + CallbackInfo cbi = mCallbacks.get(i); + if ((cbi.flags & (CALLBACK_FLAG_PERFORM_ACTIVE_SCAN + | CALLBACK_FLAG_REQUEST_DISCOVERY)) != 0) { + // Discovery explicitly requested. + routeTypes |= cbi.type; + } else if ((cbi.flags & CALLBACK_FLAG_PASSIVE_DISCOVERY) != 0) { + // Discovery only passively requested. + passiveRouteTypes |= cbi.type; + } else { + // Legacy case since applications don't specify the discovery flag. + // Unfortunately we just have to assume they always need discovery + // whenever they have a callback registered. + routeTypes |= cbi.type; + } + if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) { + activeScan = true; + if ((cbi.type & (ROUTE_TYPE_LIVE_VIDEO | ROUTE_TYPE_REMOTE_DISPLAY)) != 0) { + activeScanWifiDisplay = true; + } + } + } + if (routeTypes != 0 || activeScan) { + // If someone else requests discovery then enable the passive listeners. + // This is used by the MediaRouteButton and MediaRouteActionProvider since + // they don't receive lifecycle callbacks from the Activity. + routeTypes |= passiveRouteTypes; + } + + // Update wifi display scanning. + if (activeScanWifiDisplay) { if (!mActivelyScanningWifiDisplays) { mActivelyScanningWifiDisplays = true; mHandler.post(mScanWifiDisplays); @@ -225,18 +281,14 @@ public class MediaRouter { mHandler.removeCallbacks(mScanWifiDisplays); } } - } - private boolean hasActiveScanCallbackOfType(int type) { - final int count = mCallbacks.size(); - for (int i = 0; i < count; i++) { - CallbackInfo cbi = mCallbacks.get(i); - if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0 - && (cbi.type & type) != 0) { - return true; - } + // Tell the media router service all about it. + if (routeTypes != mDiscoveryRequestRouteTypes + || activeScan != mDiscoverRequestActiveScan) { + mDiscoveryRequestRouteTypes = routeTypes; + mDiscoverRequestActiveScan = activeScan; + publishClientDiscoveryRequest(); } - return false; } @Override @@ -271,6 +323,270 @@ public class MediaRouter { } } } + + void setSelectedRoute(RouteInfo info, boolean explicit) { + // Must be non-reentrant. + mSelectedRoute = info; + publishClientSelectedRoute(explicit); + } + + void rebindAsUser(int userId) { + if (mCurrentUserId != userId || userId < 0 || mClient == null) { + if (mClient != null) { + try { + mMediaRouterService.unregisterClient(mClient); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to unregister media router client.", ex); + } + mClient = null; + } + + mCurrentUserId = userId; + + try { + Client client = new Client(); + mMediaRouterService.registerClientAsUser(client, + mAppContext.getPackageName(), userId); + mClient = client; + } catch (RemoteException ex) { + Log.e(TAG, "Unable to register media router client.", ex); + } + + publishClientDiscoveryRequest(); + publishClientSelectedRoute(false); + updateClientState(); + } + } + + void publishClientDiscoveryRequest() { + if (mClient != null) { + try { + mMediaRouterService.setDiscoveryRequest(mClient, + mDiscoveryRequestRouteTypes, mDiscoverRequestActiveScan); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to publish media router client discovery request.", ex); + } + } + } + + void publishClientSelectedRoute(boolean explicit) { + if (mClient != null) { + try { + mMediaRouterService.setSelectedRoute(mClient, + mSelectedRoute != null ? mSelectedRoute.mGlobalRouteId : null, + explicit); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to publish media router client selected route.", ex); + } + } + } + + void updateClientState() { + // Update the client state. + mClientState = null; + if (mClient != null) { + try { + mClientState = mMediaRouterService.getState(mClient); + } catch (RemoteException ex) { + Log.e(TAG, "Unable to retrieve media router client state.", ex); + } + } + final ArrayList<MediaRouterClientState.RouteInfo> globalRoutes = + mClientState != null ? mClientState.routes : null; + final String globallySelectedRouteId = mClientState != null ? + mClientState.globallySelectedRouteId : null; + + // Add or update routes. + final int globalRouteCount = globalRoutes != null ? globalRoutes.size() : 0; + for (int i = 0; i < globalRouteCount; i++) { + final MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(i); + RouteInfo route = findGlobalRoute(globalRoute.id); + if (route == null) { + route = makeGlobalRoute(globalRoute); + addRouteStatic(route); + } else { + updateGlobalRoute(route, globalRoute); + } + } + + // Synchronize state with the globally selected route. + if (globallySelectedRouteId != null) { + final RouteInfo route = findGlobalRoute(globallySelectedRouteId); + if (route == null) { + Log.w(TAG, "Could not find new globally selected route: " + + globallySelectedRouteId); + } else if (route != mSelectedRoute) { + if (DEBUG) { + Log.d(TAG, "Selecting new globally selected route: " + route); + } + selectRouteStatic(route.mSupportedTypes, route, false); + } + } else if (mSelectedRoute != null && mSelectedRoute.mGlobalRouteId != null) { + if (DEBUG) { + Log.d(TAG, "Unselecting previous globally selected route: " + mSelectedRoute); + } + selectDefaultRouteStatic(); + } + + // Remove defunct routes. + outer: for (int i = mRoutes.size(); i-- > 0; ) { + final RouteInfo route = mRoutes.get(i); + final String globalRouteId = route.mGlobalRouteId; + if (globalRouteId != null) { + for (int j = 0; j < globalRouteCount; j++) { + MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(j); + if (globalRouteId.equals(globalRoute.id)) { + continue outer; // found + } + } + // not found + removeRouteStatic(route); + } + } + } + + void requestSetVolume(RouteInfo route, int volume) { + if (route.mGlobalRouteId != null && mClient != null) { + try { + mMediaRouterService.requestSetVolume(mClient, + route.mGlobalRouteId, volume); + } catch (RemoteException ex) { + Log.w(TAG, "Unable to request volume change.", ex); + } + } + } + + void requestUpdateVolume(RouteInfo route, int direction) { + if (route.mGlobalRouteId != null && mClient != null) { + try { + mMediaRouterService.requestUpdateVolume(mClient, + route.mGlobalRouteId, direction); + } catch (RemoteException ex) { + Log.w(TAG, "Unable to request volume change.", ex); + } + } + } + + RouteInfo makeGlobalRoute(MediaRouterClientState.RouteInfo globalRoute) { + RouteInfo route = new RouteInfo(sStatic.mSystemCategory); + route.mGlobalRouteId = globalRoute.id; + route.mName = globalRoute.name; + route.mDescription = globalRoute.description; + route.mSupportedTypes = globalRoute.supportedTypes; + route.mEnabled = globalRoute.enabled; + route.setStatusCode(globalRoute.statusCode); + route.mPlaybackType = globalRoute.playbackType; + route.mPlaybackStream = globalRoute.playbackStream; + route.mVolume = globalRoute.volume; + route.mVolumeMax = globalRoute.volumeMax; + route.mVolumeHandling = globalRoute.volumeHandling; + route.mPresentationDisplay = getDisplayForGlobalRoute(globalRoute); + return route; + } + + void updateGlobalRoute(RouteInfo route, MediaRouterClientState.RouteInfo globalRoute) { + boolean changed = false; + boolean volumeChanged = false; + boolean presentationDisplayChanged = false; + + if (!Objects.equal(route.mName, globalRoute.name)) { + route.mName = globalRoute.name; + changed = true; + } + if (!Objects.equal(route.mDescription, globalRoute.description)) { + route.mDescription = globalRoute.description; + changed = true; + } + if (route.mSupportedTypes != globalRoute.supportedTypes) { + route.mSupportedTypes = globalRoute.supportedTypes; + changed = true; + } + if (route.mEnabled != globalRoute.enabled) { + route.mEnabled = globalRoute.enabled; + changed = true; + } + if (route.mStatusCode != globalRoute.statusCode) { + route.setStatusCode(globalRoute.statusCode); + changed = true; + } + if (route.mPlaybackType != globalRoute.playbackType) { + route.mPlaybackType = globalRoute.playbackType; + changed = true; + } + if (route.mPlaybackStream != globalRoute.playbackStream) { + route.mPlaybackStream = globalRoute.playbackStream; + changed = true; + } + if (route.mVolume != globalRoute.volume) { + route.mVolume = globalRoute.volume; + changed = true; + volumeChanged = true; + } + if (route.mVolumeMax != globalRoute.volumeMax) { + route.mVolumeMax = globalRoute.volumeMax; + changed = true; + volumeChanged = true; + } + if (route.mVolumeHandling != globalRoute.volumeHandling) { + route.mVolumeHandling = globalRoute.volumeHandling; + changed = true; + volumeChanged = true; + } + final Display presentationDisplay = getDisplayForGlobalRoute(globalRoute); + if (route.mPresentationDisplay != presentationDisplay) { + route.mPresentationDisplay = presentationDisplay; + changed = true; + presentationDisplayChanged = true; + } + + if (changed) { + dispatchRouteChanged(route); + } + if (volumeChanged) { + dispatchRouteVolumeChanged(route); + } + if (presentationDisplayChanged) { + dispatchRoutePresentationDisplayChanged(route); + } + } + + Display getDisplayForGlobalRoute(MediaRouterClientState.RouteInfo globalRoute) { + // Ensure that the specified display is valid for presentations. + // This check will normally disallow the default display unless it was configured + // as a presentation display for some reason. + if (globalRoute.presentationDisplayId >= 0) { + Display display = mDisplayService.getDisplay(globalRoute.presentationDisplayId); + if (display != null && display.isPublicPresentation()) { + return display; + } + } + return null; + } + + RouteInfo findGlobalRoute(String globalRouteId) { + final int count = mRoutes.size(); + for (int i = 0; i < count; i++) { + final RouteInfo route = mRoutes.get(i); + if (globalRouteId.equals(route.mGlobalRouteId)) { + return route; + } + } + return null; + } + + final class Client extends IMediaRouterClient.Stub { + @Override + public void onStateChanged() { + mHandler.post(new Runnable() { + @Override + public void run() { + if (Client.this == mClient) { + updateClientState(); + } + } + }); + } + } } static Static sStatic; @@ -285,7 +601,7 @@ public class MediaRouter { * <p>Once initiated this routing is transparent to the application. All audio * played on the media stream will be routed to the selected destination.</p> */ - public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; + public static final int ROUTE_TYPE_LIVE_AUDIO = 1 << 0; /** * Route type flag for live video. @@ -302,7 +618,13 @@ public class MediaRouter { * @see RouteInfo#getPresentationDisplay() * @see android.app.Presentation */ - public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2; + public static final int ROUTE_TYPE_LIVE_VIDEO = 1 << 1; + + /** + * Temporary interop constant to identify remote displays. + * @hide To be removed when media router API is updated. + */ + public static final int ROUTE_TYPE_REMOTE_DISPLAY = 1 << 2; /** * Route type flag for application-specific usage. @@ -312,7 +634,10 @@ public class MediaRouter { * is expected to interpret the meaning of these events and perform the requested * routing tasks.</p> */ - public static final int ROUTE_TYPE_USER = 0x00800000; + public static final int ROUTE_TYPE_USER = 1 << 23; + + static final int ROUTE_TYPE_ANY = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO + | ROUTE_TYPE_REMOTE_DISPLAY | ROUTE_TYPE_USER; /** * Flag for {@link #addCallback}: Actively scan for routes while this callback @@ -336,11 +661,27 @@ public class MediaRouter { * Flag for {@link #addCallback}: Do not filter route events. * <p> * When this flag is specified, the callback will be invoked for event that affect any - * route event if they do not match the callback's associated media route selector. + * route even if they do not match the callback's filter. * </p> */ public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1; + /** + * Explicitly requests discovery. + * + * @hide Future API ported from support library. Revisit this later. + */ + public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2; + + /** + * Requests that discovery be performed but only if there is some other active + * callback already registered. + * + * @hide Compatibility workaround for the fact that applications do not currently + * request discovery explicitly (except when using the support library API). + */ + public static final int CALLBACK_FLAG_PASSIVE_DISCOVERY = 1 << 3; + // Maps application contexts static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>(); @@ -352,6 +693,9 @@ public class MediaRouter { if ((types & ROUTE_TYPE_LIVE_VIDEO) != 0) { result.append("ROUTE_TYPE_LIVE_VIDEO "); } + if ((types & ROUTE_TYPE_REMOTE_DISPLAY) != 0) { + result.append("ROUTE_TYPE_REMOTE_DISPLAY "); + } if ((types & ROUTE_TYPE_USER) != 0) { result.append("ROUTE_TYPE_USER "); } @@ -453,9 +797,7 @@ public class MediaRouter { info = new CallbackInfo(cb, types, flags, this); sStatic.mCallbacks.add(info); } - if ((info.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) { - sStatic.updateActiveScan(); - } + sStatic.updateDiscoveryRequest(); } /** @@ -466,10 +808,8 @@ public class MediaRouter { public void removeCallback(Callback cb) { int index = findCallbackInfo(cb); if (index >= 0) { - CallbackInfo info = sStatic.mCallbacks.remove(index); - if ((info.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) { - sStatic.updateActiveScan(); - } + sStatic.mCallbacks.remove(index); + sStatic.updateDiscoveryRequest(); } else { Log.w(TAG, "removeCallback(" + cb + "): callback not registered"); } @@ -499,17 +839,17 @@ public class MediaRouter { * @param route Route to select */ public void selectRoute(int types, RouteInfo route) { - selectRouteStatic(types, route); + selectRouteStatic(types, route, true); } - + /** * @hide internal use */ - public void selectRouteInt(int types, RouteInfo route) { - selectRouteStatic(types, route); + public void selectRouteInt(int types, RouteInfo route, boolean explicit) { + selectRouteStatic(types, route, explicit); } - static void selectRouteStatic(int types, RouteInfo route) { + static void selectRouteStatic(int types, RouteInfo route, boolean explicit) { final RouteInfo oldRoute = sStatic.mSelectedRoute; if (oldRoute == route) return; if ((route.getSupportedTypes() & types) == 0) { @@ -541,15 +881,26 @@ public class MediaRouter { } } + sStatic.setSelectedRoute(route, explicit); + if (oldRoute != null) { dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute); } - sStatic.mSelectedRoute = route; if (route != null) { dispatchRouteSelected(types & route.getSupportedTypes(), route); } } + static void selectDefaultRouteStatic() { + // TODO: Be smarter about the route types here; this selects for all valid. + if (sStatic.mSelectedRoute != sStatic.mBluetoothA2dpRoute + && sStatic.mBluetoothA2dpRoute != null) { + selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mBluetoothA2dpRoute, false); + } else { + selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mDefaultAudioVideo, false); + } + } + /** * Compare the device address of a display and a route. * Nulls/no device address will match another null/no address. @@ -612,7 +963,7 @@ public class MediaRouter { * @see #addUserRoute(UserRouteInfo) */ public void removeUserRoute(UserRouteInfo info) { - removeRoute(info); + removeRouteStatic(info); } /** @@ -626,7 +977,7 @@ public class MediaRouter { // TODO Right now, RouteGroups only ever contain user routes. // The code below will need to change if this assumption does. if (info instanceof UserRouteInfo || info instanceof RouteGroup) { - removeRouteAt(i); + removeRouteStatic(info); i--; } } @@ -636,10 +987,10 @@ public class MediaRouter { * @hide internal use only */ public void removeRouteInt(RouteInfo info) { - removeRoute(info); + removeRouteStatic(info); } - static void removeRoute(RouteInfo info) { + static void removeRouteStatic(RouteInfo info) { if (sStatic.mRoutes.remove(info)) { final RouteCategory removingCat = info.getCategory(); final int count = sStatic.mRoutes.size(); @@ -653,40 +1004,7 @@ public class MediaRouter { } if (info == sStatic.mSelectedRoute) { // Removing the currently selected route? Select the default before we remove it. - // TODO: Be smarter about the route types here; this selects for all valid. - if (info != sStatic.mBluetoothA2dpRoute && sStatic.mBluetoothA2dpRoute != null) { - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, - sStatic.mBluetoothA2dpRoute); - } else { - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_USER, - sStatic.mDefaultAudioVideo); - } - } - if (!found) { - sStatic.mCategories.remove(removingCat); - } - dispatchRouteRemoved(info); - } - } - - void removeRouteAt(int routeIndex) { - if (routeIndex >= 0 && routeIndex < sStatic.mRoutes.size()) { - final RouteInfo info = sStatic.mRoutes.remove(routeIndex); - final RouteCategory removingCat = info.getCategory(); - final int count = sStatic.mRoutes.size(); - boolean found = false; - for (int i = 0; i < count; i++) { - final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); - if (removingCat == cat) { - found = true; - break; - } - } - if (info == sStatic.mSelectedRoute) { - // Removing the currently selected route? Select the default before we remove it. - // TODO: Be smarter about the route types here; this selects for all valid. - selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO | ROUTE_TYPE_USER, - sStatic.mDefaultAudioVideo); + selectDefaultRouteStatic(); } if (!found) { sStatic.mCategories.remove(removingCat); @@ -752,7 +1070,7 @@ public class MediaRouter { * * @see #addUserRoute(UserRouteInfo) * @see #removeUserRoute(UserRouteInfo) - * @see #createRouteCategory(CharSequence) + * @see #createRouteCategory(CharSequence, boolean) */ public UserRouteInfo createUserRoute(RouteCategory category) { return new UserRouteInfo(category); @@ -780,6 +1098,23 @@ public class MediaRouter { return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable); } + /** + * Rebinds the media router to handle routes that belong to the specified user. + * Requires the interact across users permission to access the routes of another user. + * <p> + * This method is a complete hack to work around the singleton nature of the + * media router when running inside of singleton processes like QuickSettings. + * This mechanism should be burned to the ground when MediaRouter is redesigned. + * Ideally the current user would be pulled from the Context but we need to break + * down MediaRouter.Static before we can get there. + * </p> + * + * @hide + */ + public void rebindAsUser(int userId) { + sStatic.rebindAsUser(userId); + } + static void updateRoute(final RouteInfo info) { dispatchRouteChanged(info); } @@ -906,7 +1241,7 @@ public class MediaRouter { updateWifiDisplayRoute(route, d, newStatus); } if (d.equals(activeDisplay)) { - selectRouteStatic(route.getSupportedTypes(), route); + selectRouteStatic(route.getSupportedTypes(), route, false); // Don't scan if we're already connected to a wifi display, // the scanning process can cause a hiccup with some configurations. @@ -919,7 +1254,7 @@ public class MediaRouter { if (d.isRemembered()) { final WifiDisplay newDisplay = findMatchingDisplay(d, newDisplays); if (newDisplay == null || !newDisplay.isRemembered()) { - removeRoute(findWifiDisplayRoute(d)); + removeRouteStatic(findWifiDisplayRoute(d)); } } } @@ -932,8 +1267,7 @@ public class MediaRouter { } static int getWifiDisplayStatusCode(WifiDisplay d, WifiDisplayStatus wfdStatus) { - int newStatus = RouteInfo.STATUS_NONE; - + int newStatus; if (wfdStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING) { newStatus = RouteInfo.STATUS_SCANNING; } else if (d.isAvailable()) { @@ -947,7 +1281,7 @@ public class MediaRouter { final int activeState = wfdStatus.getActiveDisplayState(); switch (activeState) { case WifiDisplayStatus.DISPLAY_STATE_CONNECTED: - newStatus = RouteInfo.STATUS_NONE; + newStatus = RouteInfo.STATUS_CONNECTED; break; case WifiDisplayStatus.DISPLAY_STATE_CONNECTING: newStatus = RouteInfo.STATUS_CONNECTING; @@ -968,7 +1302,8 @@ public class MediaRouter { static RouteInfo makeWifiDisplayRoute(WifiDisplay display, WifiDisplayStatus wfdStatus) { final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory); newRoute.mDeviceAddress = display.getDeviceAddress(); - newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO; + newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO + | ROUTE_TYPE_REMOTE_DISPLAY; newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED; newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE; @@ -1004,8 +1339,7 @@ public class MediaRouter { if (!enabled && route == sStatic.mSelectedRoute) { // Oops, no longer available. Reselect the default. - final RouteInfo defaultRoute = sStatic.mDefaultAudioVideo; - selectRouteStatic(defaultRoute.getSupportedTypes(), defaultRoute); + selectDefaultRouteStatic(); } } @@ -1075,6 +1409,10 @@ public class MediaRouter { String mDeviceAddress; boolean mEnabled = true; + // An id by which the route is known to the media router service. + // Null if this route only exists as an artifact within this process. + String mGlobalRouteId; + // A predetermined connection status that can override mStatus private int mStatusCode; @@ -1084,19 +1422,20 @@ public class MediaRouter { /** @hide */ public static final int STATUS_AVAILABLE = 3; /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4; /** @hide */ public static final int STATUS_IN_USE = 5; + /** @hide */ public static final int STATUS_CONNECTED = 6; private Object mTag; /** * The default playback type, "local", indicating the presentation of the media is happening * on the same device (e.g. a phone, a tablet) as where it is controlled from. - * @see #setPlaybackType(int) + * @see #getPlaybackType() */ public final static int PLAYBACK_TYPE_LOCAL = 0; /** * A playback type indicating the presentation of the media is happening on * a different device (i.e. the remote device) than where it is controlled from. - * @see #setPlaybackType(int) + * @see #getPlaybackType() */ public final static int PLAYBACK_TYPE_REMOTE = 1; /** @@ -1104,12 +1443,13 @@ public class MediaRouter { * controlled from this object. An example of fixed playback volume is a remote player, * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather * than attenuate at the source. - * @see #setVolumeHandling(int) + * @see #getVolumeHandling() */ public final static int PLAYBACK_VOLUME_FIXED = 0; /** * Playback information indicating the playback volume is variable and can be controlled * from this object. + * @see #getVolumeHandling() */ public final static int PLAYBACK_VOLUME_VARIABLE = 1; @@ -1181,7 +1521,7 @@ public class MediaRouter { boolean setStatusCode(int statusCode) { if (statusCode != mStatusCode) { mStatusCode = statusCode; - int resId = 0; + int resId; switch (statusCode) { case STATUS_SCANNING: resId = com.android.internal.R.string.media_route_status_scanning; @@ -1198,6 +1538,11 @@ public class MediaRouter { case STATUS_IN_USE: resId = com.android.internal.R.string.media_route_status_in_use; break; + case STATUS_CONNECTED: + case STATUS_NONE: + default: + resId = 0; + break; } mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null; return true; @@ -1317,9 +1662,7 @@ public class MediaRouter { Log.e(TAG, "Error setting local stream volume", e); } } else { - Log.e(TAG, getClass().getSimpleName() + ".requestSetVolume(): " + - "Non-local volume playback on system route? " + - "Could not request volume change."); + sStatic.requestSetVolume(this, volume); } } @@ -1338,9 +1681,7 @@ public class MediaRouter { Log.e(TAG, "Error setting local stream volume", e); } } else { - Log.e(TAG, getClass().getSimpleName() + ".requestChangeVolume(): " + - "Non-local volume playback on system route? " + - "Could not request volume change."); + sStatic.requestUpdateVolume(this, direction); } } @@ -1418,7 +1759,19 @@ public class MediaRouter { * @return True if this route is in the process of connecting. */ public boolean isConnecting() { - return mStatusCode == STATUS_CONNECTING; + // If the route is selected and its status appears to be between states + // then report it as connecting even though it has not yet had a chance + // to move into the CONNECTING state. Note that routes in the NONE state + // are assumed to not require an explicit connection lifecycle. + if (this == sStatic.mSelectedRoute) { + switch (mStatusCode) { + case STATUS_AVAILABLE: + case STATUS_SCANNING: + case STATUS_CONNECTING: + return true; + } + } + return false; } void setStatusInt(CharSequence status) { @@ -1432,6 +1785,7 @@ public class MediaRouter { } final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() { + @Override public void dispatchRemoteVolumeUpdate(final int direction, final int value) { sStatic.mHandler.post(new Runnable() { @Override @@ -1460,7 +1814,7 @@ public class MediaRouter { ", status=" + getStatus() + ", category=" + getCategory() + ", supportedTypes=" + supportedTypes + - ", presentationDisplay=" + mPresentationDisplay + "}"; + ", presentationDisplay=" + mPresentationDisplay + " }"; } } @@ -1716,6 +2070,7 @@ public class MediaRouter { mVolumeHandling = PLAYBACK_VOLUME_FIXED; } + @Override CharSequence getName(Resources res) { if (mUpdateName) updateName(); return super.getName(res); @@ -1916,7 +2271,7 @@ public class MediaRouter { final int count = mRoutes.size(); if (count == 0) { // Don't keep empty groups in the router. - MediaRouter.removeRoute(this); + MediaRouter.removeRouteStatic(this); return; } @@ -2071,6 +2426,7 @@ public class MediaRouter { return mIsSystem; } + @Override public String toString() { return "RouteCategory{ name=" + mName + " types=" + typesToString(mTypes) + " groupable=" + mGroupable + " }"; diff --git a/media/java/android/media/MediaRouterClientState.aidl b/media/java/android/media/MediaRouterClientState.aidl new file mode 100644 index 0000000..70077119 --- /dev/null +++ b/media/java/android/media/MediaRouterClientState.aidl @@ -0,0 +1,18 @@ +/* Copyright 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.media; + +parcelable MediaRouterClientState; diff --git a/media/java/android/media/MediaRouterClientState.java b/media/java/android/media/MediaRouterClientState.java new file mode 100644 index 0000000..0847503 --- /dev/null +++ b/media/java/android/media/MediaRouterClientState.java @@ -0,0 +1,183 @@ +/* + * 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.media; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; + +/** + * Information available from MediaRouterService about the state perceived by + * a particular client and the routes that are available to it. + * + * Clients must not modify the contents of this object. + * @hide + */ +public final class MediaRouterClientState implements Parcelable { + /** + * A list of all known routes. + */ + public final ArrayList<RouteInfo> routes; + + /** + * The id of the current globally selected route, or null if none. + * Globally selected routes override any other route selections that applications + * may have made. Used for remote displays. + */ + public String globallySelectedRouteId; + + public MediaRouterClientState() { + routes = new ArrayList<RouteInfo>(); + } + + MediaRouterClientState(Parcel src) { + routes = src.createTypedArrayList(RouteInfo.CREATOR); + globallySelectedRouteId = src.readString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedList(routes); + dest.writeString(globallySelectedRouteId); + } + + public static final Parcelable.Creator<MediaRouterClientState> CREATOR = + new Parcelable.Creator<MediaRouterClientState>() { + @Override + public MediaRouterClientState createFromParcel(Parcel in) { + return new MediaRouterClientState(in); + } + + @Override + public MediaRouterClientState[] newArray(int size) { + return new MediaRouterClientState[size]; + } + }; + + public static final class RouteInfo implements Parcelable { + public String id; + public String name; + public String description; + public int supportedTypes; + public boolean enabled; + public int statusCode; + public int playbackType; + public int playbackStream; + public int volume; + public int volumeMax; + public int volumeHandling; + public int presentationDisplayId; + + public RouteInfo(String id) { + this.id = id; + enabled = true; + statusCode = MediaRouter.RouteInfo.STATUS_NONE; + playbackType = MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE; + playbackStream = -1; + volumeHandling = MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED; + presentationDisplayId = -1; + } + + public RouteInfo(RouteInfo other) { + id = other.id; + name = other.name; + description = other.description; + supportedTypes = other.supportedTypes; + enabled = other.enabled; + statusCode = other.statusCode; + playbackType = other.playbackType; + playbackStream = other.playbackStream; + volume = other.volume; + volumeMax = other.volumeMax; + volumeHandling = other.volumeHandling; + presentationDisplayId = other.presentationDisplayId; + } + + RouteInfo(Parcel in) { + id = in.readString(); + name = in.readString(); + description = in.readString(); + supportedTypes = in.readInt(); + enabled = in.readInt() != 0; + statusCode = in.readInt(); + playbackType = in.readInt(); + playbackStream = in.readInt(); + volume = in.readInt(); + volumeMax = in.readInt(); + volumeHandling = in.readInt(); + presentationDisplayId = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(name); + dest.writeString(description); + dest.writeInt(supportedTypes); + dest.writeInt(enabled ? 1 : 0); + dest.writeInt(statusCode); + dest.writeInt(playbackType); + dest.writeInt(playbackStream); + dest.writeInt(volume); + dest.writeInt(volumeMax); + dest.writeInt(volumeHandling); + dest.writeInt(presentationDisplayId); + } + + @Override + public String toString() { + return "RouteInfo{ id=" + id + + ", name=" + name + + ", description=" + description + + ", supportedTypes=0x" + Integer.toHexString(supportedTypes) + + ", enabled=" + enabled + + ", statusCode=" + statusCode + + ", playbackType=" + playbackType + + ", playbackStream=" + playbackStream + + ", volume=" + volume + + ", volumeMax=" + volumeMax + + ", volumeHandling=" + volumeHandling + + ", presentationDisplayId=" + presentationDisplayId + + " }"; + } + + @SuppressWarnings("hiding") + public static final Parcelable.Creator<RouteInfo> CREATOR = + new Parcelable.Creator<RouteInfo>() { + @Override + public RouteInfo createFromParcel(Parcel in) { + return new RouteInfo(in); + } + + @Override + public RouteInfo[] newArray(int size) { + return new RouteInfo[size]; + } + }; + } +} diff --git a/media/java/android/media/RemoteDisplayState.aidl b/media/java/android/media/RemoteDisplayState.aidl new file mode 100644 index 0000000..b3262fc --- /dev/null +++ b/media/java/android/media/RemoteDisplayState.aidl @@ -0,0 +1,18 @@ +/* Copyright 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.media; + +parcelable RemoteDisplayState; diff --git a/media/java/android/media/RemoteDisplayState.java b/media/java/android/media/RemoteDisplayState.java new file mode 100644 index 0000000..1197f65 --- /dev/null +++ b/media/java/android/media/RemoteDisplayState.java @@ -0,0 +1,189 @@ +/* + * 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.media; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; + +/** + * Information available from IRemoteDisplayProvider about available remote displays. + * + * Clients must not modify the contents of this object. + * @hide + */ +public final class RemoteDisplayState implements Parcelable { + // Note: These constants are used by the remote display provider API. + // Do not change them! + public static final String SERVICE_INTERFACE = + "com.android.media.remotedisplay.RemoteDisplayProvider"; + public static final int DISCOVERY_MODE_NONE = 0; + public static final int DISCOVERY_MODE_PASSIVE = 1; + public static final int DISCOVERY_MODE_ACTIVE = 2; + + /** + * A list of all remote displays. + */ + public final ArrayList<RemoteDisplayInfo> displays; + + public RemoteDisplayState() { + displays = new ArrayList<RemoteDisplayInfo>(); + } + + RemoteDisplayState(Parcel src) { + displays = src.createTypedArrayList(RemoteDisplayInfo.CREATOR); + } + + public boolean isValid() { + if (displays == null) { + return false; + } + final int count = displays.size(); + for (int i = 0; i < count; i++) { + if (!displays.get(i).isValid()) { + return false; + } + } + return true; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedList(displays); + } + + public static final Parcelable.Creator<RemoteDisplayState> CREATOR = + new Parcelable.Creator<RemoteDisplayState>() { + @Override + public RemoteDisplayState createFromParcel(Parcel in) { + return new RemoteDisplayState(in); + } + + @Override + public RemoteDisplayState[] newArray(int size) { + return new RemoteDisplayState[size]; + } + }; + + public static final class RemoteDisplayInfo implements Parcelable { + // Note: These constants are used by the remote display provider API. + // Do not change them! + public static final int STATUS_NOT_AVAILABLE = 0; + public static final int STATUS_IN_USE = 1; + public static final int STATUS_AVAILABLE = 2; + public static final int STATUS_CONNECTING = 3; + public static final int STATUS_CONNECTED = 4; + + public static final int PLAYBACK_VOLUME_VARIABLE = + MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE; + public static final int PLAYBACK_VOLUME_FIXED = + MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED; + + public String id; + public String name; + public String description; + public int status; + public int volume; + public int volumeMax; + public int volumeHandling; + public int presentationDisplayId; + + public RemoteDisplayInfo(String id) { + this.id = id; + status = STATUS_NOT_AVAILABLE; + volumeHandling = MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED; + presentationDisplayId = -1; + } + + public RemoteDisplayInfo(RemoteDisplayInfo other) { + id = other.id; + name = other.name; + description = other.description; + status = other.status; + volume = other.volume; + volumeMax = other.volumeMax; + volumeHandling = other.volumeHandling; + presentationDisplayId = other.presentationDisplayId; + } + + RemoteDisplayInfo(Parcel in) { + id = in.readString(); + name = in.readString(); + description = in.readString(); + status = in.readInt(); + volume = in.readInt(); + volumeMax = in.readInt(); + volumeHandling = in.readInt(); + presentationDisplayId = in.readInt(); + } + + public boolean isValid() { + return !TextUtils.isEmpty(id) && !TextUtils.isEmpty(name); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeString(name); + dest.writeString(description); + dest.writeInt(status); + dest.writeInt(volume); + dest.writeInt(volumeMax); + dest.writeInt(volumeHandling); + dest.writeInt(presentationDisplayId); + } + + @Override + public String toString() { + return "RemoteDisplayInfo{ id=" + id + + ", name=" + name + + ", description=" + description + + ", status=" + status + + ", volume=" + volume + + ", volumeMax=" + volumeMax + + ", volumeHandling=" + volumeHandling + + ", presentationDisplayId=" + presentationDisplayId + + " }"; + } + + @SuppressWarnings("hiding") + public static final Parcelable.Creator<RemoteDisplayInfo> CREATOR = + new Parcelable.Creator<RemoteDisplayInfo>() { + @Override + public RemoteDisplayInfo createFromParcel(Parcel in) { + return new RemoteDisplayInfo(in); + } + + @Override + public RemoteDisplayInfo[] newArray(int size) { + return new RemoteDisplayInfo[size]; + } + }; + } +} diff --git a/media/lib/Android.mk b/media/lib/Android.mk new file mode 100644 index 0000000..50799a6 --- /dev/null +++ b/media/lib/Android.mk @@ -0,0 +1,46 @@ +# +# 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) + +# the library +# ============================================================ +include $(CLEAR_VARS) + +LOCAL_MODULE:= com.android.media.remotedisplay +LOCAL_MODULE_TAGS := optional + +LOCAL_SRC_FILES := \ + $(call all-subdir-java-files) \ + $(call all-aidl-files-under, java) + +include $(BUILD_JAVA_LIBRARY) + + +# ==== com.android.media.remotedisplay.xml lib def ======================== +include $(CLEAR_VARS) + +LOCAL_MODULE := com.android.media.remotedisplay.xml +LOCAL_MODULE_TAGS := optional + +LOCAL_MODULE_CLASS := ETC + +# This will install the file in /system/etc/permissions +# +LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions + +LOCAL_SRC_FILES := $(LOCAL_MODULE) + +include $(BUILD_PREBUILT) diff --git a/media/lib/README.txt b/media/lib/README.txt new file mode 100644 index 0000000..cade3df --- /dev/null +++ b/media/lib/README.txt @@ -0,0 +1,28 @@ +This library (com.android.media.remotedisplay.jar) is a shared java library +containing classes required by unbundled remote display providers. + +--- Rules of this library --- +o This library is effectively a PUBLIC API for unbundled remote display providers + that may be distributed outside the system image. So it MUST BE API STABLE. + You can add but not remove. The rules are the same as for the + public platform SDK API. +o This library can see and instantiate internal platform classes, but it must not + expose them in any public method (or by extending them via inheritance). This would + break clients of the library because they cannot see the internal platform classes. + +This library is distributed in the system image, and loaded as +a shared library. So you can change the implementation, but not +the interface. In this way it is like framework.jar. + +--- Why does this library exists? --- + +Unbundled remote display providers (such as Cast) cannot use internal +platform classes. + +This library will eventually be replaced when the media route provider +infrastructure that is currently defined in the support library is reintegrated +with the framework in a new API. That API isn't ready yet so this +library is a compromise to make new capabilities available to the system +without exposing the full surface area of the support library media +route provider protocol. + diff --git a/media/lib/com.android.media.remotedisplay.xml b/media/lib/com.android.media.remotedisplay.xml new file mode 100644 index 0000000..77a91d2 --- /dev/null +++ b/media/lib/com.android.media.remotedisplay.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. +--> + +<permissions> + <library name="com.android.media.remotedisplay" + file="/system/framework/com.android.media.remotedisplay.jar" /> +</permissions> diff --git a/media/lib/java/com/android/media/remotedisplay/RemoteDisplay.java b/media/lib/java/com/android/media/remotedisplay/RemoteDisplay.java new file mode 100644 index 0000000..5e15702 --- /dev/null +++ b/media/lib/java/com/android/media/remotedisplay/RemoteDisplay.java @@ -0,0 +1,173 @@ +/* + * 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.media.remotedisplay; + +import com.android.internal.util.Objects; + +import android.media.MediaRouter; +import android.media.RemoteDisplayState.RemoteDisplayInfo; +import android.text.TextUtils; + +/** + * Represents a remote display that has been discovered. + */ +public class RemoteDisplay { + private final RemoteDisplayInfo mMutableInfo; + private RemoteDisplayInfo mImmutableInfo; + + /** + * Status code: Indicates that the remote display is not available. + */ + public static final int STATUS_NOT_AVAILABLE = RemoteDisplayInfo.STATUS_NOT_AVAILABLE; + + /** + * Status code: Indicates that the remote display is in use by someone else. + */ + public static final int STATUS_IN_USE = RemoteDisplayInfo.STATUS_IN_USE; + + /** + * Status code: Indicates that the remote display is available for new connections. + */ + public static final int STATUS_AVAILABLE = RemoteDisplayInfo.STATUS_AVAILABLE; + + /** + * Status code: Indicates that the remote display is current connecting. + */ + public static final int STATUS_CONNECTING = RemoteDisplayInfo.STATUS_CONNECTING; + + /** + * Status code: Indicates that the remote display is connected and is mirroring + * display contents. + */ + public static final int STATUS_CONNECTED = RemoteDisplayInfo.STATUS_CONNECTED; + + /** + * Volume handling: Output volume can be changed. + */ + public static final int PLAYBACK_VOLUME_VARIABLE = + RemoteDisplayInfo.PLAYBACK_VOLUME_VARIABLE; + + /** + * Volume handling: Output volume is fixed. + */ + public static final int PLAYBACK_VOLUME_FIXED = + RemoteDisplayInfo.PLAYBACK_VOLUME_FIXED; + + /** + * Creates a remote display with the specified name and id. + */ + public RemoteDisplay(String id, String name) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("id must not be null or empty"); + } + mMutableInfo = new RemoteDisplayInfo(id); + setName(name); + } + + public String getId() { + return mMutableInfo.id; + } + + public String getName() { + return mMutableInfo.name; + } + + public void setName(String name) { + if (!Objects.equal(mMutableInfo.name, name)) { + mMutableInfo.name = name; + mImmutableInfo = null; + } + } + + public String getDescription() { + return mMutableInfo.description; + } + + public void setDescription(String description) { + if (!Objects.equal(mMutableInfo.description, description)) { + mMutableInfo.description = description; + mImmutableInfo = null; + } + } + + public int getStatus() { + return mMutableInfo.status; + } + + public void setStatus(int status) { + if (mMutableInfo.status != status) { + mMutableInfo.status = status; + mImmutableInfo = null; + } + } + + public int getVolume() { + return mMutableInfo.volume; + } + + public void setVolume(int volume) { + if (mMutableInfo.volume != volume) { + mMutableInfo.volume = volume; + mImmutableInfo = null; + } + } + + public int getVolumeMax() { + return mMutableInfo.volumeMax; + } + + public void setVolumeMax(int volumeMax) { + if (mMutableInfo.volumeMax != volumeMax) { + mMutableInfo.volumeMax = volumeMax; + mImmutableInfo = null; + } + } + + public int getVolumeHandling() { + return mMutableInfo.volumeHandling; + } + + public void setVolumeHandling(int volumeHandling) { + if (mMutableInfo.volumeHandling != volumeHandling) { + mMutableInfo.volumeHandling = volumeHandling; + mImmutableInfo = null; + } + } + + public int getPresentationDisplayId() { + return mMutableInfo.presentationDisplayId; + } + + public void setPresentationDisplayId(int presentationDisplayId) { + if (mMutableInfo.presentationDisplayId != presentationDisplayId) { + mMutableInfo.presentationDisplayId = presentationDisplayId; + mImmutableInfo = null; + } + } + + @Override + public String toString() { + return "RemoteDisplay{" + mMutableInfo.toString() + "}"; + } + + RemoteDisplayInfo getInfo() { + if (mImmutableInfo == null) { + mImmutableInfo = new RemoteDisplayInfo(mMutableInfo); + } + return mImmutableInfo; + } +} diff --git a/media/lib/java/com/android/media/remotedisplay/RemoteDisplayProvider.java b/media/lib/java/com/android/media/remotedisplay/RemoteDisplayProvider.java new file mode 100644 index 0000000..8e4042c --- /dev/null +++ b/media/lib/java/com/android/media/remotedisplay/RemoteDisplayProvider.java @@ -0,0 +1,371 @@ +/* + * 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.media.remotedisplay; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.media.IRemoteDisplayCallback; +import android.media.IRemoteDisplayProvider; +import android.media.RemoteDisplayState; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.ArrayMap; + +import java.util.Collection; + +/** + * Base class for remote display providers implemented as unbundled services. + * <p> + * To implement your remote display provider service, create a subclass of + * {@link Service} and override the {@link Service#onBind Service.onBind()} method + * to return the provider's binder when the {@link #SERVICE_INTERFACE} is requested. + * </p> + * <pre> + * public class SampleRemoteDisplayProviderService extends Service { + * private SampleProvider mProvider; + * + * public IBinder onBind(Intent intent) { + * if (intent.getAction().equals(RemoteDisplayProvider.SERVICE_INTERFACE)) { + * if (mProvider == null) { + * mProvider = new SampleProvider(this); + * } + * return mProvider.getBinder(); + * } + * return null; + * } + * + * class SampleProvider extends RemoteDisplayProvider { + * public SampleProvider() { + * super(SampleRemoteDisplayProviderService.this); + * } + * + * // --- Implementation goes here --- + * } + * } + * </pre> + * <p> + * Declare your remote display provider service in your application manifest + * like this: + * </p> + * <pre> + * <application> + * <uses-library android:name="com.android.media.remotedisplay" /> + * + * <service android:name=".SampleRemoteDisplayProviderService" + * android:label="@string/sample_remote_display_provider_service" + * android:exported="true" + * android:permission="android.permission.BIND_REMOTE_DISPLAY"> + * <intent-filter> + * <action android:name="com.android.media.remotedisplay.RemoteDisplayProvider" /> + * </intent-filter> + * </service> + * </application> + * </pre> + * <p> + * This object is not thread safe. It is only intended to be accessed on the + * {@link Context#getMainLooper main looper thread} of an application. + * </p><p> + * IMPORTANT: This class is effectively a public API for unbundled applications, and + * must remain API stable. See README.txt in the root of this package for more information. + * </p> + */ +public abstract class RemoteDisplayProvider { + private static final int MSG_SET_CALLBACK = 1; + private static final int MSG_SET_DISCOVERY_MODE = 2; + private static final int MSG_CONNECT = 3; + private static final int MSG_DISCONNECT = 4; + private static final int MSG_SET_VOLUME = 5; + private static final int MSG_ADJUST_VOLUME = 6; + + private final ProviderStub mStub; + private final ProviderHandler mHandler; + private final ArrayMap<String, RemoteDisplay> mDisplays = + new ArrayMap<String, RemoteDisplay>(); + private IRemoteDisplayCallback mCallback; + private int mDiscoveryMode = DISCOVERY_MODE_NONE; + + /** + * The {@link Intent} that must be declared as handled by the service. + * Put this in your manifest. + */ + public static final String SERVICE_INTERFACE = RemoteDisplayState.SERVICE_INTERFACE; + + /** + * Discovery mode: Do not perform any discovery. + */ + public static final int DISCOVERY_MODE_NONE = RemoteDisplayState.DISCOVERY_MODE_NONE; + + /** + * Discovery mode: Passive or low-power periodic discovery. + * <p> + * This mode indicates that an application is interested in knowing whether there + * are any remote displays paired or available but doesn't need the latest or + * most detailed information. The provider may scan at a lower rate or rely on + * knowledge of previously paired devices. + * </p> + */ + public static final int DISCOVERY_MODE_PASSIVE = RemoteDisplayState.DISCOVERY_MODE_PASSIVE; + + /** + * Discovery mode: Active discovery. + * <p> + * This mode indicates that the user is actively trying to connect to a route + * and we should perform continuous scans. This mode may use significantly more + * power but is intended to be short-lived. + * </p> + */ + public static final int DISCOVERY_MODE_ACTIVE = RemoteDisplayState.DISCOVERY_MODE_ACTIVE; + + /** + * Creates a remote display provider. + * + * @param context The application context for the remote display provider. + */ + public RemoteDisplayProvider(Context context) { + mStub = new ProviderStub(); + mHandler = new ProviderHandler(context.getMainLooper()); + } + + /** + * Gets the Binder associated with the provider. + * <p> + * This is intended to be used for the onBind() method of a service that implements + * a remote display provider service. + * </p> + * + * @return The IBinder instance associated with the provider. + */ + public IBinder getBinder() { + return mStub; + } + + /** + * Called when the current discovery mode changes. + * + * @param mode The new discovery mode. + */ + public void onDiscoveryModeChanged(int mode) { + } + + /** + * Called when the system would like to connect to a display. + * + * @param display The remote display. + */ + public void onConnect(RemoteDisplay display) { + } + + /** + * Called when the system would like to disconnect from a display. + * + * @param display The remote display. + */ + public void onDisconnect(RemoteDisplay display) { + } + + /** + * Called when the system would like to set the volume of a display. + * + * @param display The remote display. + * @param volume The desired volume. + */ + public void onSetVolume(RemoteDisplay display, int volume) { + } + + /** + * Called when the system would like to adjust the volume of a display. + * + * @param display The remote display. + * @param delta An increment to add to the current volume, such as +1 or -1. + */ + public void onAdjustVolume(RemoteDisplay display, int delta) { + } + + /** + * Gets the current discovery mode. + * + * @return The current discovery mode. + */ + public int getDiscoveryMode() { + return mDiscoveryMode; + } + + /** + * Gets the current collection of displays. + * + * @return The current collection of displays, which must not be modified. + */ + public Collection<RemoteDisplay> getDisplays() { + return mDisplays.values(); + } + + /** + * Adds the specified remote display and notifies the system. + * + * @param display The remote display that was added. + * @throws IllegalStateException if there is already a display with the same id. + */ + public void addDisplay(RemoteDisplay display) { + if (display == null || mDisplays.containsKey(display.getId())) { + throw new IllegalArgumentException("display"); + } + mDisplays.put(display.getId(), display); + publishState(); + } + + /** + * Updates information about the specified remote display and notifies the system. + * + * @param display The remote display that was added. + * @throws IllegalStateException if the display was n + */ + public void updateDisplay(RemoteDisplay display) { + if (display == null || mDisplays.get(display.getId()) != display) { + throw new IllegalArgumentException("display"); + } + publishState(); + } + + /** + * Removes the specified remote display and tells the system about it. + * + * @param display The remote display that was removed. + */ + public void removeDisplay(RemoteDisplay display) { + if (display == null || mDisplays.get(display.getId()) != display) { + throw new IllegalArgumentException("display"); + } + mDisplays.remove(display.getId()); + publishState(); + } + + void setCallback(IRemoteDisplayCallback callback) { + mCallback = callback; + publishState(); + } + + void setDiscoveryMode(int mode) { + if (mDiscoveryMode != mode) { + mDiscoveryMode = mode; + onDiscoveryModeChanged(mode); + } + } + + void publishState() { + if (mCallback != null) { + RemoteDisplayState state = new RemoteDisplayState(); + final int count = mDisplays.size(); + for (int i = 0; i < count; i++) { + final RemoteDisplay display = mDisplays.valueAt(i); + state.displays.add(display.getInfo()); + } + try { + mCallback.onStateChanged(state); + } catch (RemoteException ex) { + // system server died? + } + } + } + + RemoteDisplay findRemoteDisplay(String id) { + return mDisplays.get(id); + } + + final class ProviderStub extends IRemoteDisplayProvider.Stub { + @Override + public void setCallback(IRemoteDisplayCallback callback) { + mHandler.obtainMessage(MSG_SET_CALLBACK, callback).sendToTarget(); + } + + @Override + public void setDiscoveryMode(int mode) { + mHandler.obtainMessage(MSG_SET_DISCOVERY_MODE, mode, 0).sendToTarget(); + } + + @Override + public void connect(String id) { + mHandler.obtainMessage(MSG_CONNECT, id).sendToTarget(); + } + + @Override + public void disconnect(String id) { + mHandler.obtainMessage(MSG_DISCONNECT, id).sendToTarget(); + } + + @Override + public void setVolume(String id, int volume) { + mHandler.obtainMessage(MSG_SET_VOLUME, volume, 0, id).sendToTarget(); + } + + @Override + public void adjustVolume(String id, int delta) { + mHandler.obtainMessage(MSG_ADJUST_VOLUME, delta, 0, id).sendToTarget(); + } + } + + final class ProviderHandler extends Handler { + public ProviderHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SET_CALLBACK: { + setCallback((IRemoteDisplayCallback)msg.obj); + break; + } + case MSG_SET_DISCOVERY_MODE: { + setDiscoveryMode(msg.arg1); + break; + } + case MSG_CONNECT: { + RemoteDisplay display = findRemoteDisplay((String)msg.obj); + if (display != null) { + onConnect(display); + } + break; + } + case MSG_DISCONNECT: { + RemoteDisplay display = findRemoteDisplay((String)msg.obj); + if (display != null) { + onDisconnect(display); + } + break; + } + case MSG_SET_VOLUME: { + RemoteDisplay display = findRemoteDisplay((String)msg.obj); + if (display != null) { + onSetVolume(display, msg.arg1); + } + break; + } + case MSG_ADJUST_VOLUME: { + RemoteDisplay display = findRemoteDisplay((String)msg.obj); + if (display != null) { + onAdjustVolume(display, msg.arg1); + } + break; + } + } + } + } +} diff --git a/packages/Keyguard/AndroidManifest.xml b/packages/Keyguard/AndroidManifest.xml index 9e296e2..66d1e75 100644 --- a/packages/Keyguard/AndroidManifest.xml +++ b/packages/Keyguard/AndroidManifest.xml @@ -38,6 +38,7 @@ <uses-permission android:name="android.permission.BIND_DEVICE_ADMIN" /> <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" /> <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" /> + <uses-permission android:name="android.permission.ACCESS_KEYGUARD_SECURE_STORAGE" /> <application android:label="@string/app_name" android:process="com.android.systemui" diff --git a/packages/Keyguard/res/layout/keyguard_presentation.xml b/packages/Keyguard/res/layout/keyguard_presentation.xml new file mode 100644 index 0000000..7df0b70 --- /dev/null +++ b/packages/Keyguard/res/layout/keyguard_presentation.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 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. +*/ +--> + +<!-- This is a view that shows general status information in Keyguard. --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/res/com.android.keyguard" + android:id="@+id/presentation" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.android.keyguard.KeyguardStatusView + android:id="@+id/clock" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/keyguard_accessibility_status"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal|top" + android:orientation="vertical" + android:focusable="true"> + <TextClock + android:id="@+id/clock_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal|top" + android:textColor="@color/clock_white" + android:singleLine="true" + style="@style/widget_big_thin" + android:format12Hour="@string/keyguard_widget_12_hours_format" + android:format24Hour="@string/keyguard_widget_24_hours_format" + android:baselineAligned="true" + android:layout_marginBottom="@dimen/bottom_text_spacing_digital" /> + + <include layout="@layout/keyguard_status_area" /> + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="10dip" + android:layout_gravity="center_horizontal" + android:src="@drawable/kg_security_lock_normal" /> + </LinearLayout> + </com.android.keyguard.KeyguardStatusView> + +</FrameLayout> diff --git a/packages/Keyguard/res/layout/keyguard_status_view.xml b/packages/Keyguard/res/layout/keyguard_status_view.xml index 5857fc2..a4d298a 100644 --- a/packages/Keyguard/res/layout/keyguard_status_view.xml +++ b/packages/Keyguard/res/layout/keyguard_status_view.xml @@ -26,7 +26,7 @@ android:layout_height="match_parent" androidprv:layout_maxWidth="@dimen/keyguard_security_width" androidprv:layout_maxHeight="@dimen/keyguard_security_height" - android:gravity="center_horizontal"> + android:gravity="center"> <com.android.keyguard.KeyguardStatusView android:id="@+id/keyguard_status_view_face_palm" diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/Keyguard/src/com/android/keyguard/KeyguardDisplayManager.java new file mode 100644 index 0000000..6bcbd6c --- /dev/null +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardDisplayManager.java @@ -0,0 +1,171 @@ +/* + * 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.keyguard; + +import android.app.Presentation; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.graphics.Point; +import android.media.MediaRouter; +import android.media.MediaRouter.RouteInfo; +import android.os.Bundle; +import android.util.Slog; +import android.view.Display; +import android.view.View; +import android.view.WindowManager; + +public class KeyguardDisplayManager { + protected static final String TAG = "KeyguardDisplayManager"; + private static boolean DEBUG = KeyguardViewMediator.DEBUG; + Presentation mPresentation; + private MediaRouter mMediaRouter; + private Context mContext; + private boolean mShowing; + + KeyguardDisplayManager(Context context) { + mContext = context; + mMediaRouter = (MediaRouter) mContext.getSystemService(Context.MEDIA_ROUTER_SERVICE); + } + + void show() { + if (!mShowing) { + if (DEBUG) Slog.v(TAG, "show"); + mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, + mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); + updateDisplays(true); + } + mShowing = true; + } + + void hide() { + if (mShowing) { + if (DEBUG) Slog.v(TAG, "hide"); + mMediaRouter.removeCallback(mMediaRouterCallback); + updateDisplays(false); + } + mShowing = false; + } + + private final MediaRouter.SimpleCallback mMediaRouterCallback = + new MediaRouter.SimpleCallback() { + @Override + public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { + if (DEBUG) Slog.d(TAG, "onRouteSelected: type=" + type + ", info=" + info); + updateDisplays(mShowing); + } + + @Override + public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { + if (DEBUG) Slog.d(TAG, "onRouteUnselected: type=" + type + ", info=" + info); + updateDisplays(mShowing); + } + + @Override + public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) { + if (DEBUG) Slog.d(TAG, "onRoutePresentationDisplayChanged: info=" + info); + updateDisplays(mShowing); + } + }; + + private OnDismissListener mOnDismissListener = new OnDismissListener() { + + @Override + public void onDismiss(DialogInterface dialog) { + mPresentation = null; + } + }; + + protected void updateDisplays(boolean showing) { + if (showing) { + MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute( + MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY); + boolean useDisplay = route != null + && route.getPlaybackType() == MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE; + Display presentationDisplay = useDisplay ? route.getPresentationDisplay() : null; + + if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) { + if (DEBUG) Slog.v(TAG, "Display gone: " + mPresentation.getDisplay()); + mPresentation.dismiss(); + mPresentation = null; + } + + if (mPresentation == null && presentationDisplay != null) { + if (DEBUG) Slog.i(TAG, "Keyguard enabled on display: " + presentationDisplay); + mPresentation = new KeyguardPresentation(mContext, presentationDisplay); + mPresentation.setOnDismissListener(mOnDismissListener); + try { + mPresentation.show(); + } catch (WindowManager.InvalidDisplayException ex) { + Slog.w(TAG, "Invalid display:", ex); + mPresentation = null; + } + } + } else { + if (mPresentation != null) { + mPresentation.dismiss(); + mPresentation = null; + } + } + } + + private final static class KeyguardPresentation extends Presentation { + private static final int VIDEO_SAFE_REGION = 80; // Percentage of display width & height + private static final int MOVE_CLOCK_TIMEOUT = 10000; // 10s + private View mClock; + private int mUsableWidth; + private int mUsableHeight; + private int mMarginTop; + private int mMarginLeft; + Runnable mMoveTextRunnable = new Runnable() { + @Override + public void run() { + int x = mMarginLeft + (int) (Math.random() * (mUsableWidth - mClock.getWidth())); + int y = mMarginTop + (int) (Math.random() * (mUsableHeight - mClock.getHeight())); + mClock.setTranslationX(x); + mClock.setTranslationY(y); + mClock.postDelayed(mMoveTextRunnable, MOVE_CLOCK_TIMEOUT); + } + }; + + public KeyguardPresentation(Context context, Display display) { + super(context, display); + getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); + } + + public void onDetachedFromWindow() { + mClock.removeCallbacks(mMoveTextRunnable); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Point p = new Point(); + getDisplay().getSize(p); + mUsableWidth = VIDEO_SAFE_REGION * p.x/100; + mUsableHeight = VIDEO_SAFE_REGION * p.y/100; + mMarginLeft = (100 - VIDEO_SAFE_REGION) * p.x / 200; + mMarginTop = (100 - VIDEO_SAFE_REGION) * p.y / 200; + + setContentView(R.layout.keyguard_presentation); + mClock = findViewById(R.id.clock); + + // Avoid screen burn in + mClock.post(mMoveTextRunnable); + } + } +} diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java b/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java index 2719c5c..349078f 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardTransportControlView.java @@ -60,12 +60,12 @@ import java.util.TimeZone; */ public class KeyguardTransportControlView extends FrameLayout { - private static final int DISPLAY_TIMEOUT_MS = 5000; // 5s private static final int RESET_TO_METADATA_DELAY = 5000; protected static final boolean DEBUG = false; protected static final String TAG = "TransportControlView"; private static final boolean ANIMATE_TRANSITIONS = true; + protected static final long QUIESCENT_PLAYBACK_FACTOR = 1000; private ViewGroup mMetadataContainer; private ViewGroup mInfoContainer; @@ -89,11 +89,9 @@ public class KeyguardTransportControlView extends FrameLayout { private ImageView mBadge; private boolean mSeekEnabled; - private boolean mUserSeeking; private java.text.DateFormat mFormat; - private Date mTimeElapsed; - private Date mTrackDuration; + private Date mTempDate = new Date(); /** * The metadata which should be populated into the view once we've been attached @@ -111,18 +109,25 @@ public class KeyguardTransportControlView extends FrameLayout { @Override public void onClientPlaybackStateUpdate(int state) { - setSeekBarsEnabled(false); updatePlayPauseState(state); } @Override public void onClientPlaybackStateUpdate(int state, long stateChangeTimeMs, long currentPosMs, float speed) { - setSeekBarsEnabled(mMetadata != null && mMetadata.duration > 0); updatePlayPauseState(state); if (DEBUG) Log.d(TAG, "onClientPlaybackStateUpdate(state=" + state + ", stateChangeTimeMs=" + stateChangeTimeMs + ", currentPosMs=" + currentPosMs + ", speed=" + speed + ")"); + + removeCallbacks(mUpdateSeekBars); + // Since the music client may be responding to historical events that cause the + // playback state to change dramatically, wait until things become quiescent before + // resuming automatic scrub position update. + if (mTransientSeek.getVisibility() == View.VISIBLE + && playbackPositionShouldMove(mCurrentPlayState)) { + postDelayed(mUpdateSeekBars, QUIESCENT_PLAYBACK_FACTOR); + } } @Override @@ -136,15 +141,21 @@ public class KeyguardTransportControlView extends FrameLayout { } }; - private final Runnable mUpdateSeekBars = new Runnable() { + private class UpdateSeekBarRunnable implements Runnable { public void run() { - if (updateSeekBars()) { + boolean seekAble = updateOnce(); + if (seekAble) { removeCallbacks(this); postDelayed(this, 1000); } } + public boolean updateOnce() { + return updateSeekBars(); + } }; + private final UpdateSeekBarRunnable mUpdateSeekBars = new UpdateSeekBarRunnable(); + private final Runnable mResetToMetadata = new Runnable() { public void run() { resetToMetadata(); @@ -163,6 +174,7 @@ public class KeyguardTransportControlView extends FrameLayout { } if (keyCode != -1) { sendMediaButtonClick(keyCode); + delayResetToMetadata(); // if the scrub bar is showing, keep showing it. } } }; @@ -177,25 +189,67 @@ public class KeyguardTransportControlView extends FrameLayout { } }; + // This class is here to throttle scrub position updates to the music client + class FutureSeekRunnable implements Runnable { + private int mProgress; + private boolean mPending; + + public void run() { + scrubTo(mProgress); + mPending = false; + } + + void setProgress(int progress) { + mProgress = progress; + if (!mPending) { + mPending = true; + postDelayed(this, 30); + } + } + }; + + // This is here because RemoteControlClient's method isn't visible :/ + private final static boolean playbackPositionShouldMove(int playstate) { + switch(playstate) { + case RemoteControlClient.PLAYSTATE_STOPPED: + case RemoteControlClient.PLAYSTATE_PAUSED: + case RemoteControlClient.PLAYSTATE_BUFFERING: + case RemoteControlClient.PLAYSTATE_ERROR: + case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS: + case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS: + return false; + case RemoteControlClient.PLAYSTATE_PLAYING: + case RemoteControlClient.PLAYSTATE_FAST_FORWARDING: + case RemoteControlClient.PLAYSTATE_REWINDING: + default: + return true; + } + } + + private final FutureSeekRunnable mFutureSeekRunnable = new FutureSeekRunnable(); + private final SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (fromUser) { - scrubTo(progress); + mFutureSeekRunnable.setProgress(progress); delayResetToMetadata(); + mTempDate.setTime(progress); + mTransientSeekTimeElapsed.setText(mFormat.format(mTempDate)); + } else { + updateSeekDisplay(); } - updateSeekDisplay(); } @Override public void onStartTrackingTouch(SeekBar seekBar) { - mUserSeeking = true; + delayResetToMetadata(); + removeCallbacks(mUpdateSeekBars); // don't update during user interaction } @Override public void onStopTrackingTouch(SeekBar seekBar) { - mUserSeeking = false; } }; @@ -247,17 +301,11 @@ public class KeyguardTransportControlView extends FrameLayout { if (enabled == mSeekEnabled) return; mSeekEnabled = enabled; - if (mTransientSeek.getVisibility() == VISIBLE) { + if (mTransientSeek.getVisibility() == VISIBLE && !enabled) { mTransientSeek.setVisibility(INVISIBLE); mMetadataContainer.setVisibility(VISIBLE); - mUserSeeking = false; cancelResetToMetadata(); } - if (enabled) { - mUpdateSeekBars.run(); - } else { - removeCallbacks(mUpdateSeekBars); - } } public void setTransportControlCallback(KeyguardHostView.TransportControlCallback @@ -294,6 +342,8 @@ public class KeyguardTransportControlView extends FrameLayout { } final boolean screenOn = KeyguardUpdateMonitor.getInstance(mContext).isScreenOn(); setEnableMarquee(screenOn); + // Allow long-press anywhere else in this view to show the seek bar + setOnLongClickListener(mTransportShowSeekBarListener); } @Override @@ -326,7 +376,6 @@ public class KeyguardTransportControlView extends FrameLayout { mAudioManager.unregisterRemoteController(mRemoteController); KeyguardUpdateMonitor.getInstance(mContext).removeCallback(mUpdateMonitor); mMetadata.clear(); - mUserSeeking = false; removeCallbacks(mUpdateSeekBars); } @@ -484,18 +533,12 @@ public class KeyguardTransportControlView extends FrameLayout { void updateSeekDisplay() { if (mMetadata != null && mRemoteController != null && mFormat != null) { - if (mTimeElapsed == null) { - mTimeElapsed = new Date(); - } - if (mTrackDuration == null) { - mTrackDuration = new Date(); - } - mTimeElapsed.setTime(mRemoteController.getEstimatedMediaPosition()); - mTrackDuration.setTime(mMetadata.duration); - mTransientSeekTimeElapsed.setText(mFormat.format(mTimeElapsed)); - mTransientSeekTimeTotal.setText(mFormat.format(mTrackDuration)); + mTempDate.setTime(mRemoteController.getEstimatedMediaPosition()); + mTransientSeekTimeElapsed.setText(mFormat.format(mTempDate)); + mTempDate.setTime(mMetadata.duration); + mTransientSeekTimeTotal.setText(mFormat.format(mTempDate)); - if (DEBUG) Log.d(TAG, "updateSeekDisplay timeElapsed=" + mTimeElapsed + + if (DEBUG) Log.d(TAG, "updateSeekDisplay timeElapsed=" + mTempDate + " duration=" + mMetadata.duration); } } @@ -508,10 +551,16 @@ public class KeyguardTransportControlView extends FrameLayout { mTransientSeek.setVisibility(INVISIBLE); mMetadataContainer.setVisibility(VISIBLE); cancelResetToMetadata(); + removeCallbacks(mUpdateSeekBars); // don't update if scrubber isn't visible } else { mTransientSeek.setVisibility(VISIBLE); mMetadataContainer.setVisibility(INVISIBLE); delayResetToMetadata(); + if (playbackPositionShouldMove(mCurrentPlayState)) { + mUpdateSeekBars.run(); + } else { + mUpdateSeekBars.updateOnce(); + } } mTransportControlCallback.userActivity(); return true; @@ -573,9 +622,6 @@ public class KeyguardTransportControlView extends FrameLayout { case RemoteControlClient.PLAYSTATE_PLAYING: imageResId = R.drawable.ic_media_pause; imageDescId = R.string.keyguard_transport_pause_description; - if (mSeekEnabled) { - mUpdateSeekBars.run(); - } break; case RemoteControlClient.PLAYSTATE_BUFFERING: @@ -590,10 +636,9 @@ public class KeyguardTransportControlView extends FrameLayout { break; } - if (state != RemoteControlClient.PLAYSTATE_PLAYING) { - removeCallbacks(mUpdateSeekBars); - updateSeekBars(); - } + boolean clientSupportsSeek = mMetadata != null && mMetadata.duration > 0; + setSeekBarsEnabled(clientSupportsSeek); + mBtnPlay.setImageResource(imageResId); mBtnPlay.setContentDescription(getResources().getString(imageDescId)); mCurrentPlayState = state; @@ -601,11 +646,9 @@ public class KeyguardTransportControlView extends FrameLayout { boolean updateSeekBars() { final int position = (int) mRemoteController.getEstimatedMediaPosition(); + if (DEBUG) Log.v(TAG, "Estimated time:" + position); if (position >= 0) { - if (DEBUG) Log.v(TAG, "Seek to " + position); - if (!mUserSeeking) { - mTransientSeekBar.setProgress(position); - } + mTransientSeekBar.setProgress(position); return true; } Log.w(TAG, "Updating seek bars; received invalid estimated media position (" + @@ -671,34 +714,4 @@ public class KeyguardTransportControlView extends FrameLayout { public boolean providesClock() { return false; } - - private boolean wasPlayingRecently(int state, long stateChangeTimeMs) { - switch (state) { - case RemoteControlClient.PLAYSTATE_PLAYING: - case RemoteControlClient.PLAYSTATE_FAST_FORWARDING: - case RemoteControlClient.PLAYSTATE_REWINDING: - case RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS: - case RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS: - case RemoteControlClient.PLAYSTATE_BUFFERING: - // actively playing or about to play - return true; - case RemoteControlClient.PLAYSTATE_NONE: - return false; - case RemoteControlClient.PLAYSTATE_STOPPED: - case RemoteControlClient.PLAYSTATE_PAUSED: - case RemoteControlClient.PLAYSTATE_ERROR: - // we have stopped playing, check how long ago - if (DEBUG) { - if ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS) { - Log.v(TAG, "wasPlayingRecently: time < TIMEOUT was playing recently"); - } else { - Log.v(TAG, "wasPlayingRecently: time > TIMEOUT"); - } - } - return ((SystemClock.elapsedRealtime() - stateChangeTimeMs) < DISPLAY_TIMEOUT_MS); - default: - Log.e(TAG, "Unknown playback state " + state + " in wasPlayingRecently()"); - return false; - } - } } diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java index fd7cae6..6aa0a4b 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewManager.java @@ -61,9 +61,11 @@ import android.widget.FrameLayout; public class KeyguardViewManager { private final static boolean DEBUG = KeyguardViewMediator.DEBUG; private static String TAG = "KeyguardViewManager"; - public static boolean USE_UPPER_CASE = true; public final static String IS_SWITCHING_USER = "is_switching_user"; + // Delay dismissing keyguard to allow animations to complete. + private static final int HIDE_KEYGUARD_DELAY = 500; + // Timeout used for keypresses static final int DIGIT_PRESS_WAKE_MILLIS = 5000; @@ -509,9 +511,10 @@ public class KeyguardViewManager { mKeyguardHost.setCustomBackground(null); updateShowWallpaper(true); mKeyguardHost.removeView(lastView); + mViewMediatorCallback.keyguardGone(); } } - }, 500); + }, HIDE_KEYGUARD_DELAY); } } } diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java b/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java index b92ae90..49982ea 100644 --- a/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java +++ b/packages/Keyguard/src/com/android/keyguard/KeyguardViewMediator.java @@ -253,6 +253,11 @@ public class KeyguardViewMediator { private final float mLockSoundVolume; /** + * For managing external displays + */ + private KeyguardDisplayManager mKeyguardDisplayManager; + + /** * Cache of avatar drawables, for use by KeyguardMultiUserAvatar. */ private static MultiUserAvatarCache sMultiUserAvatarCache = new MultiUserAvatarCache(); @@ -304,6 +309,11 @@ public class KeyguardViewMediator { * Report that the keyguard is dismissable, pending the next keyguardDone call. */ void keyguardDonePending(); + + /** + * Report when keyguard is actually gone + */ + void keyguardGone(); } KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { @@ -457,6 +467,11 @@ public class KeyguardViewMediator { public void keyguardDonePending() { mKeyguardDonePending = true; } + + @Override + public void keyguardGone() { + mKeyguardDisplayManager.hide(); + } }; private void userActivity() { @@ -483,6 +498,8 @@ public class KeyguardViewMediator { mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(DELAYED_KEYGUARD_ACTION)); + mKeyguardDisplayManager = new KeyguardDisplayManager(context); + mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mUpdateMonitor = KeyguardUpdateMonitor.getInstance(context); @@ -597,6 +614,7 @@ public class KeyguardViewMediator { } } KeyguardUpdateMonitor.getInstance(mContext).dispatchScreenTurndOff(why); + mKeyguardDisplayManager.show(); } private void doKeyguardLaterLocked() { @@ -1218,6 +1236,7 @@ public class KeyguardViewMediator { mShowKeyguardWakeLock.release(); } + mKeyguardDisplayManager.show(); } /** diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 556a146..25202ed 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -490,10 +490,8 @@ <string name="quick_settings_wifi_no_network">No Network</string> <!-- QuickSettings: Wifi (Off) [CHAR LIMIT=NONE] --> <string name="quick_settings_wifi_off_label">Wi-Fi Off</string> - <!-- QuickSettings: Wifi display [CHAR LIMIT=NONE] --> - <string name="quick_settings_wifi_display_label">Wi-Fi Display</string> - <!-- QuickSettings: Wifi display [CHAR LIMIT=NONE] --> - <string name="quick_settings_wifi_display_no_connection_label">Wireless Display</string> + <!-- QuickSettings: Remote display [CHAR LIMIT=NONE] --> + <string name="quick_settings_remote_display_no_connection_label">Cast Screen</string> <!-- QuickSettings: Brightness dialog title [CHAR LIMIT=NONE] --> <string name="quick_settings_brightness_dialog_title">Brightness</string> <!-- QuickSettings: Brightness dialog auto brightness button [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 607ce41..bbac4ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -2105,9 +2105,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode { } public void tickerHalting() { - mStatusBarContents.setVisibility(View.VISIBLE); + if (mStatusBarContents.getVisibility() != View.VISIBLE) { + mStatusBarContents.setVisibility(View.VISIBLE); + mStatusBarContents + .startAnimation(loadAnim(com.android.internal.R.anim.fade_in, null)); + } mTickerView.setVisibility(View.GONE); - mStatusBarContents.startAnimation(loadAnim(com.android.internal.R.anim.fade_in, null)); // we do not animate the ticker away at this point, just get rid of it (b/6992707) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java index 9a5f43f..bb37837 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettings.java @@ -40,7 +40,6 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LevelListDrawable; import android.hardware.display.DisplayManager; -import android.hardware.display.WifiDisplayStatus; import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.Handler; @@ -94,9 +93,7 @@ class QuickSettings { private QuickSettingsModel mModel; private ViewGroup mContainerView; - private DisplayManager mDisplayManager; private DevicePolicyManager mDevicePolicyManager; - private WifiDisplayStatus mWifiDisplayStatus; private PhoneStatusBar mStatusBarService; private BluetoothState mBluetoothState; private BluetoothAdapter mBluetoothAdapter; @@ -120,13 +117,11 @@ class QuickSettings { new ArrayList<QuickSettingsTileView>(); public QuickSettings(Context context, QuickSettingsContainerView container) { - mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); mDevicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); mContext = context; mContainerView = container; mModel = new QuickSettingsModel(context); - mWifiDisplayStatus = new WifiDisplayStatus(); mBluetoothState = new QuickSettingsModel.BluetoothState(); mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); @@ -173,7 +168,6 @@ class QuickSettings { mLocationController = locationController; setupQuickSettings(); - updateWifiDisplayStatus(); updateResources(); applyLocationEnabledStatus(); @@ -707,20 +701,20 @@ class QuickSettings { }); parent.addView(alarmTile); - // Wifi Display - QuickSettingsBasicTile wifiDisplayTile + // Remote Display + QuickSettingsBasicTile remoteDisplayTile = new QuickSettingsBasicTile(mContext); - wifiDisplayTile.setImageResource(R.drawable.ic_qs_remote_display); - wifiDisplayTile.setOnClickListener(new View.OnClickListener() { + remoteDisplayTile.setImageResource(R.drawable.ic_qs_remote_display); + remoteDisplayTile.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startSettingsActivity(android.provider.Settings.ACTION_WIFI_DISPLAY_SETTINGS); } }); - mModel.addWifiDisplayTile(wifiDisplayTile, - new QuickSettingsModel.BasicRefreshCallback(wifiDisplayTile) + mModel.addRemoteDisplayTile(remoteDisplayTile, + new QuickSettingsModel.BasicRefreshCallback(remoteDisplayTile) .setShowWhenEnabled(true)); - parent.addView(wifiDisplayTile); + parent.addView(remoteDisplayTile); if (SHOW_IME_TILE || DEBUG_GONE_TILES) { // IME @@ -855,15 +849,6 @@ class QuickSettings { dialog.show(); } - private void updateWifiDisplayStatus() { - mWifiDisplayStatus = mDisplayManager.getWifiDisplayStatus(); - applyWifiDisplayStatus(); - } - - private void applyWifiDisplayStatus() { - mModel.onWifiDisplayStateChanged(mWifiDisplayStatus); - } - private void applyBluetoothStatus() { mModel.onBluetoothStateChange(mBluetoothState); } @@ -887,12 +872,7 @@ class QuickSettings { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); - if (DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED.equals(action)) { - WifiDisplayStatus status = (WifiDisplayStatus)intent.getParcelableExtra( - DisplayManager.EXTRA_WIFI_DISPLAY_STATUS); - mWifiDisplayStatus = status; - applyWifiDisplayStatus(); - } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { + if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); mBluetoothState.enabled = (state == BluetoothAdapter.STATE_ON); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java index fa97a11..d8950f4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsModel.java @@ -27,7 +27,8 @@ import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; import android.graphics.drawable.Drawable; -import android.hardware.display.WifiDisplayStatus; +import android.media.MediaRouter; +import android.media.MediaRouter.RouteInfo; import android.net.ConnectivityManager; import android.net.Uri; import android.os.Handler; @@ -58,7 +59,6 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, BrightnessStateChangeCallback, RotationLockControllerCallback, LocationSettingsChangeCallback { - // Sett InputMethoManagerService private static final String TAG_TRY_SUPPRESSING_IME_SWITCHER = "TrySuppressingImeSwitcher"; @@ -294,6 +294,30 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, } } + /** Callback for changes to remote display routes. */ + private class RemoteDisplayRouteCallback extends MediaRouter.SimpleCallback { + @Override + public void onRouteAdded(MediaRouter router, RouteInfo route) { + updateRemoteDisplays(); + } + @Override + public void onRouteChanged(MediaRouter router, RouteInfo route) { + updateRemoteDisplays(); + } + @Override + public void onRouteRemoved(MediaRouter router, RouteInfo route) { + updateRemoteDisplays(); + } + @Override + public void onRouteSelected(MediaRouter router, int type, RouteInfo route) { + updateRemoteDisplays(); + } + @Override + public void onRouteUnselected(MediaRouter router, int type, RouteInfo route) { + updateRemoteDisplays(); + } + } + private final Context mContext; private final Handler mHandler; private final CurrentUserTracker mUserTracker; @@ -304,6 +328,9 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, private final DisplayContrastObserver mContrastObserver; private final DisplayColorSpaceObserver mColorSpaceObserver; + private final MediaRouter mMediaRouter; + private final RemoteDisplayRouteCallback mRemoteDisplayRouteCallback; + private final boolean mHasMobileData; private QuickSettingsTileView mUserTile; @@ -326,9 +353,9 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, private RefreshCallback mWifiCallback; private WifiState mWifiState = new WifiState(); - private QuickSettingsTileView mWifiDisplayTile; - private RefreshCallback mWifiDisplayCallback; - private State mWifiDisplayState = new State(); + private QuickSettingsTileView mRemoteDisplayTile; + private RefreshCallback mRemoteDisplayCallback; + private State mRemoteDisplayState = new State(); private QuickSettingsTileView mRSSITile; private RefreshCallback mRSSICallback; @@ -401,6 +428,7 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, onColorSpaceChanged(); onNextAlarmChanged(); onBugreportChanged(); + rebindMediaRouterAsCurrentUser(); } }; @@ -417,6 +445,11 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, mColorSpaceObserver = new DisplayColorSpaceObserver(mHandler); mColorSpaceObserver.startObserving(); + mMediaRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE); + rebindMediaRouterAsCurrentUser(); + + mRemoteDisplayRouteCallback = new RemoteDisplayRouteCallback(); + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); mHasMobileData = cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE); @@ -744,24 +777,59 @@ class QuickSettingsModel implements BluetoothStateChangeCallback, mBugreportCallback.refreshView(mBugreportTile, mBugreportState); } - // Wifi Display - void addWifiDisplayTile(QuickSettingsTileView view, RefreshCallback cb) { - mWifiDisplayTile = view; - mWifiDisplayCallback = cb; + // Remote Display + void addRemoteDisplayTile(QuickSettingsTileView view, RefreshCallback cb) { + mRemoteDisplayTile = view; + mRemoteDisplayCallback = cb; + final int[] count = new int[1]; + mRemoteDisplayTile.setOnPrepareListener(new QuickSettingsTileView.OnPrepareListener() { + @Override + public void onPrepare() { + mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, + mRemoteDisplayRouteCallback, + MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); + updateRemoteDisplays(); + } + @Override + public void onUnprepare() { + mMediaRouter.removeCallback(mRemoteDisplayRouteCallback); + } + }); + + updateRemoteDisplays(); } - public void onWifiDisplayStateChanged(WifiDisplayStatus status) { - mWifiDisplayState.enabled = - (status.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON); - if (status.getActiveDisplay() != null) { - mWifiDisplayState.label = status.getActiveDisplay().getFriendlyDisplayName(); - mWifiDisplayState.iconId = R.drawable.ic_qs_remote_display_connected; - } else { - mWifiDisplayState.label = mContext.getString( - R.string.quick_settings_wifi_display_no_connection_label); - mWifiDisplayState.iconId = R.drawable.ic_qs_remote_display; + + private void rebindMediaRouterAsCurrentUser() { + mMediaRouter.rebindAsUser(mUserTracker.getCurrentUserId()); + } + + private void updateRemoteDisplays() { + MediaRouter.RouteInfo connectedRoute = mMediaRouter.getSelectedRoute( + MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY); + boolean enabled = connectedRoute != null && (connectedRoute.getSupportedTypes() + & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0; + if (!enabled) { + connectedRoute = null; + final int count = mMediaRouter.getRouteCount(); + for (int i = 0; i < count; i++) { + MediaRouter.RouteInfo route = mMediaRouter.getRouteAt(i); + if ((route.getSupportedTypes() & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) { + enabled = true; + break; + } + } } - mWifiDisplayCallback.refreshView(mWifiDisplayTile, mWifiDisplayState); + mRemoteDisplayState.enabled = enabled; + if (connectedRoute != null) { + mRemoteDisplayState.label = connectedRoute.getName().toString(); + mRemoteDisplayState.iconId = R.drawable.ic_qs_remote_display_connected; + } else { + mRemoteDisplayState.label = mContext.getString( + R.string.quick_settings_remote_display_no_connection_label); + mRemoteDisplayState.iconId = R.drawable.ic_qs_remote_display; + } + mRemoteDisplayCallback.refreshView(mRemoteDisplayTile, mRemoteDisplayState); } // IME diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsTileView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsTileView.java index 3d520f7..ad18294 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsTileView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickSettingsTileView.java @@ -21,6 +21,7 @@ import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewParent; import android.widget.FrameLayout; /** @@ -31,14 +32,14 @@ class QuickSettingsTileView extends FrameLayout { private int mContentLayoutId; private int mColSpan; - private int mRowSpan; + private boolean mPrepared; + private OnPrepareListener mOnPrepareListener; public QuickSettingsTileView(Context context, AttributeSet attrs) { super(context, attrs); mContentLayoutId = -1; mColSpan = 1; - mRowSpan = 1; } void setColumnSpan(int span) { @@ -77,4 +78,72 @@ class QuickSettingsTileView extends FrameLayout { } super.setVisibility(vis); } + + public void setOnPrepareListener(OnPrepareListener listener) { + if (mOnPrepareListener != listener) { + mOnPrepareListener = listener; + mPrepared = false; + post(new Runnable() { + @Override + public void run() { + updatePreparedState(); + } + }); + } + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + updatePreparedState(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + updatePreparedState(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + updatePreparedState(); + } + + private void updatePreparedState() { + if (mOnPrepareListener != null) { + if (isParentVisible()) { + if (!mPrepared) { + mPrepared = true; + mOnPrepareListener.onPrepare(); + } + } else if (mPrepared) { + mPrepared = false; + mOnPrepareListener.onUnprepare(); + } + } + } + + private boolean isParentVisible() { + if (!isAttachedToWindow()) { + return false; + } + for (ViewParent current = getParent(); current instanceof View; + current = current.getParent()) { + View view = (View)current; + if (view.getVisibility() != VISIBLE) { + return false; + } + } + return true; + } + + /** + * Called when the view's parent becomes visible or invisible to provide + * an opportunity for the client to provide new content. + */ + public interface OnPrepareListener { + void onPrepare(); + void onUnprepare(); + } }
\ No newline at end of file diff --git a/policy/src/com/android/internal/policy/impl/BarController.java b/policy/src/com/android/internal/policy/impl/BarController.java index c38ad04..0ce4b12 100644 --- a/policy/src/com/android/internal/policy/impl/BarController.java +++ b/policy/src/com/android/internal/policy/impl/BarController.java @@ -37,8 +37,9 @@ public class BarController { private static final boolean DEBUG = false; private static final int TRANSIENT_BAR_NONE = 0; - private static final int TRANSIENT_BAR_SHOWING = 1; - private static final int TRANSIENT_BAR_HIDING = 2; + private static final int TRANSIENT_BAR_SHOW_REQUESTED = 1; + private static final int TRANSIENT_BAR_SHOWING = 2; + private static final int TRANSIENT_BAR_HIDING = 3; private static final int TRANSLUCENT_ANIMATION_DELAY_MS = 1000; @@ -73,13 +74,9 @@ public class BarController { mWin = win; } - public boolean isHidden() { - return mState == StatusBarManager.WINDOW_STATE_HIDDEN; - } - public void showTransient() { if (mWin != null) { - setTransientBarState(TRANSIENT_BAR_SHOWING); + setTransientBarState(TRANSIENT_BAR_SHOW_REQUESTED); } } @@ -87,6 +84,10 @@ public class BarController { return mTransientBarState == TRANSIENT_BAR_SHOWING; } + public boolean isTransientShowRequested() { + return mTransientBarState == TRANSIENT_BAR_SHOW_REQUESTED; + } + public boolean wasRecentlyTranslucent() { return (SystemClock.uptimeMillis() - mLastTranslucent) < TRANSLUCENT_ANIMATION_DELAY_MS; } @@ -198,6 +199,9 @@ public class BarController { if (mTransientBarState == TRANSIENT_BAR_SHOWING) { if (DEBUG) Slog.d(mTag, "Not showing transient bar, already shown"); return false; + } else if (mTransientBarState == TRANSIENT_BAR_SHOW_REQUESTED) { + if (DEBUG) Slog.d(mTag, "Not showing transient bar, already requested"); + return false; } else if (mWin == null) { if (DEBUG) Slog.d(mTag, "Not showing transient bar, bar doesn't exist"); return false; @@ -211,12 +215,13 @@ public class BarController { public int updateVisibilityLw(boolean transientAllowed, int oldVis, int vis) { if (mWin == null) return vis; - if (mTransientBarState == TRANSIENT_BAR_SHOWING) { // transient bar requested + if (isTransientShowing() || isTransientShowRequested()) { // transient bar requested if (transientAllowed) { vis |= mTransientFlag; if ((oldVis & mTransientFlag) == 0) { vis |= mUnhideFlag; // tell sysui we're ready to unhide } + setTransientBarState(TRANSIENT_BAR_SHOWING); // request accepted } else { setTransientBarState(TRANSIENT_BAR_NONE); // request denied } @@ -254,6 +259,7 @@ public class BarController { private static String transientBarStateToString(int state) { if (state == TRANSIENT_BAR_HIDING) return "TRANSIENT_BAR_HIDING"; if (state == TRANSIENT_BAR_SHOWING) return "TRANSIENT_BAR_SHOWING"; + if (state == TRANSIENT_BAR_SHOW_REQUESTED) return "TRANSIENT_BAR_SHOW_REQUESTED"; if (state == TRANSIENT_BAR_NONE) return "TRANSIENT_BAR_NONE"; throw new IllegalArgumentException("Unknown state " + state); } diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java index d653920..8f391b1 100644 --- a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java +++ b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java @@ -308,17 +308,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { WindowState mFocusedWindow; IApplicationToken mFocusedApp; - private final class PointerLocationPointerEventListener implements PointerEventListener { - @Override - public void onPointerEvent(MotionEvent motionEvent) { - if (mPointerLocationView != null) { - mPointerLocationView.addPointerEvent(motionEvent); - } - } - } - - // Pointer location view state, only modified on the mHandler Looper. - PointerLocationPointerEventListener mPointerLocationPointerEventListener; PointerLocationView mPointerLocationView; // The current size of the screen; really; extends into the overscan area of @@ -1190,7 +1179,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (mPointerLocationView == null) { mPointerLocationView = new PointerLocationView(mContext); mPointerLocationView.setPrintCoords(false); - WindowManager.LayoutParams lp = new WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT); @@ -1210,22 +1198,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { mContext.getSystemService(Context.WINDOW_SERVICE); lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; wm.addView(mPointerLocationView, lp); - - mPointerLocationPointerEventListener = new PointerLocationPointerEventListener(); - mWindowManagerFuncs.registerPointerEventListener(mPointerLocationPointerEventListener); + mWindowManagerFuncs.registerPointerEventListener(mPointerLocationView); } } private void disablePointerLocation() { - if (mPointerLocationPointerEventListener != null) { - mWindowManagerFuncs.unregisterPointerEventListener( - mPointerLocationPointerEventListener); - mPointerLocationPointerEventListener = null; - } - if (mPointerLocationView != null) { - WindowManager wm = (WindowManager) - mContext.getSystemService(Context.WINDOW_SERVICE); + mWindowManagerFuncs.unregisterPointerEventListener(mPointerLocationView); + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); wm.removeView(mPointerLocationView); mPointerLocationView = null; } @@ -5166,9 +5146,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { mNavigationBar != null && hideNavBarSysui && immersiveSticky; - boolean denyTransientStatus = mStatusBarController.isTransientShowing() + boolean denyTransientStatus = mStatusBarController.isTransientShowRequested() && !transientStatusBarAllowed && hideStatusBarSysui; - boolean denyTransientNav = mNavigationBarController.isTransientShowing() + boolean denyTransientNav = mNavigationBarController.isTransientShowRequested() && !transientNavBarAllowed; if (denyTransientStatus || denyTransientNav) { // clear the clearable flags instead diff --git a/services/java/com/android/server/LockSettingsService.java b/services/java/com/android/server/LockSettingsService.java index cd746cf..35e7afa 100644 --- a/services/java/com/android/server/LockSettingsService.java +++ b/services/java/com/android/server/LockSettingsService.java @@ -154,11 +154,11 @@ public class LockSettingsService extends ILockSettings.Stub { } private final void checkWritePermission(int userId) { - mContext.checkCallingOrSelfPermission(PERMISSION); + mContext.enforceCallingOrSelfPermission(PERMISSION, "LockSettingsWrite"); } private final void checkPasswordReadPermission(int userId) { - mContext.checkCallingOrSelfPermission(PERMISSION); + mContext.enforceCallingOrSelfPermission(PERMISSION, "LockSettingsRead"); } private final void checkReadPermission(String requestedKey, int userId) { diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index bb14259..a42cbcf 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -55,6 +55,7 @@ import com.android.server.content.ContentService; import com.android.server.display.DisplayManagerService; import com.android.server.dreams.DreamManagerService; import com.android.server.input.InputManagerService; +import com.android.server.media.MediaRouterService; import com.android.server.net.NetworkPolicyManagerService; import com.android.server.net.NetworkStatsService; import com.android.server.os.SchedulingPolicyService; @@ -356,6 +357,7 @@ class ServerThread { DreamManagerService dreamy = null; AssetAtlasService atlas = null; PrintManagerService printManager = null; + MediaRouterService mediaRouter = null; // Bring up services needed for UI. if (factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL) { @@ -804,6 +806,16 @@ class ServerThread { } catch (Throwable e) { reportWtf("starting Print Service", e); } + + if (!disableNonCoreServices) { + try { + Slog.i(TAG, "Media Router Service"); + mediaRouter = new MediaRouterService(context); + ServiceManager.addService(Context.MEDIA_ROUTER_SERVICE, mediaRouter); + } catch (Throwable e) { + reportWtf("starting MediaRouterService", e); + } + } } // Before things start rolling, be sure we have decided whether @@ -916,6 +928,7 @@ class ServerThread { final InputManagerService inputManagerF = inputManager; final TelephonyRegistry telephonyRegistryF = telephonyRegistry; final PrintManagerService printManagerF = printManager; + final MediaRouterService mediaRouterF = mediaRouter; // We now tell the activity manager it is okay to run third party // code. It will call back into us once it has gotten to the state @@ -1063,6 +1076,12 @@ class ServerThread { } catch (Throwable e) { reportWtf("Notifying PrintManagerService running", e); } + + try { + if (mediaRouterF != null) mediaRouterF.systemRunning(); + } catch (Throwable e) { + reportWtf("Notifying MediaRouterService running", e); + } } }); diff --git a/services/java/com/android/server/input/InputManagerService.java b/services/java/com/android/server/input/InputManagerService.java index d749e6c..3145805 100644 --- a/services/java/com/android/server/input/InputManagerService.java +++ b/services/java/com/android/server/input/InputManagerService.java @@ -294,6 +294,7 @@ public class InputManagerService extends IInputManager.Stub IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); filter.addDataScheme("package"); mContext.registerReceiver(new BroadcastReceiver() { @Override diff --git a/services/java/com/android/server/media/MediaRouterService.java b/services/java/com/android/server/media/MediaRouterService.java new file mode 100644 index 0000000..2caab40 --- /dev/null +++ b/services/java/com/android/server/media/MediaRouterService.java @@ -0,0 +1,1351 @@ +/* + * 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.media; + +import com.android.internal.util.Objects; +import com.android.server.Watchdog; + +import android.Manifest; +import android.app.ActivityManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.media.AudioSystem; +import android.media.IMediaRouterClient; +import android.media.IMediaRouterService; +import android.media.MediaRouter; +import android.media.MediaRouterClientState; +import android.media.RemoteDisplayState; +import android.media.RemoteDisplayState.RemoteDisplayInfo; +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.SystemClock; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.TimeUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Provides a mechanism for discovering media routes and manages media playback + * behalf of applications. + * <p> + * Currently supports discovering remote displays via remote display provider + * services that have been registered by applications. + * </p> + */ +public final class MediaRouterService extends IMediaRouterService.Stub + implements Watchdog.Monitor { + private static final String TAG = "MediaRouterService"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + /** + * Timeout in milliseconds for a selected route to transition from a + * disconnected state to a connecting state. If we don't observe any + * progress within this interval, then we will give up and unselect the route. + */ + static final long CONNECTING_TIMEOUT = 5000; + + /** + * Timeout in milliseconds for a selected route to transition from a + * connecting state to a connected state. If we don't observe any + * progress within this interval, then we will give up and unselect the route. + */ + static final long CONNECTED_TIMEOUT = 60000; + + private final Context mContext; + + // State guarded by mLock. + private final Object mLock = new Object(); + private final SparseArray<UserRecord> mUserRecords = new SparseArray<UserRecord>(); + private final ArrayMap<IBinder, ClientRecord> mAllClientRecords = + new ArrayMap<IBinder, ClientRecord>(); + private int mCurrentUserId = -1; + + public MediaRouterService(Context context) { + mContext = context; + Watchdog.getInstance().addMonitor(this); + } + + public void systemRunning() { + IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED); + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_USER_SWITCHED)) { + switchUser(); + } + } + }, filter); + + switchUser(); + } + + @Override + public void monitor() { + synchronized (mLock) { /* check for deadlock */ } + } + + // Binder call + @Override + public void registerClientAsUser(IMediaRouterClient client, String packageName, int userId) { + if (client == null) { + throw new IllegalArgumentException("client must not be null"); + } + + final int uid = Binder.getCallingUid(); + if (!validatePackageName(uid, packageName)) { + throw new SecurityException("packageName must match the calling uid"); + } + + final int pid = Binder.getCallingPid(); + final int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId, + false /*allowAll*/, true /*requireFull*/, "registerClientAsUser", packageName); + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + registerClientLocked(client, pid, packageName, resolvedUserId); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Binder call + @Override + public void unregisterClient(IMediaRouterClient client) { + if (client == null) { + throw new IllegalArgumentException("client must not be null"); + } + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + unregisterClientLocked(client, false); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Binder call + @Override + public MediaRouterClientState getState(IMediaRouterClient client) { + if (client == null) { + throw new IllegalArgumentException("client must not be null"); + } + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + return getStateLocked(client); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Binder call + @Override + public void setDiscoveryRequest(IMediaRouterClient client, + int routeTypes, boolean activeScan) { + if (client == null) { + throw new IllegalArgumentException("client must not be null"); + } + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + setDiscoveryRequestLocked(client, routeTypes, activeScan); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Binder call + // A null routeId means that the client wants to unselect its current route. + // The explicit flag indicates whether the change was explicitly requested by the + // user or the application which may cause changes to propagate out to the rest + // of the system. Should be false when the change is in response to a new globally + // selected route or a default selection. + @Override + public void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit) { + if (client == null) { + throw new IllegalArgumentException("client must not be null"); + } + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + setSelectedRouteLocked(client, routeId, explicit); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Binder call + @Override + public void requestSetVolume(IMediaRouterClient client, String routeId, int volume) { + if (client == null) { + throw new IllegalArgumentException("client must not be null"); + } + if (routeId == null) { + throw new IllegalArgumentException("routeId must not be null"); + } + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + requestSetVolumeLocked(client, routeId, volume); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Binder call + @Override + public void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction) { + if (client == null) { + throw new IllegalArgumentException("client must not be null"); + } + if (routeId == null) { + throw new IllegalArgumentException("routeId must not be null"); + } + + final long token = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + requestUpdateVolumeLocked(client, routeId, direction); + } + } finally { + Binder.restoreCallingIdentity(token); + } + } + + // Binder call + @Override + public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump MediaRouterService from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + pw.println("MEDIA ROUTER SERVICE (dumpsys media_router)"); + pw.println(); + pw.println("Global state"); + pw.println(" mCurrentUserId=" + mCurrentUserId); + + synchronized (mLock) { + final int count = mUserRecords.size(); + for (int i = 0; i < count; i++) { + UserRecord userRecord = mUserRecords.valueAt(i); + pw.println(); + userRecord.dump(pw, ""); + } + } + } + + void switchUser() { + synchronized (mLock) { + int userId = ActivityManager.getCurrentUser(); + if (mCurrentUserId != userId) { + final int oldUserId = mCurrentUserId; + mCurrentUserId = userId; // do this first + + UserRecord oldUser = mUserRecords.get(oldUserId); + if (oldUser != null) { + oldUser.mHandler.sendEmptyMessage(UserHandler.MSG_STOP); + disposeUserIfNeededLocked(oldUser); // since no longer current user + } + + UserRecord newUser = mUserRecords.get(userId); + if (newUser != null) { + newUser.mHandler.sendEmptyMessage(UserHandler.MSG_START); + } + } + } + } + + void clientDied(ClientRecord clientRecord) { + synchronized (mLock) { + unregisterClientLocked(clientRecord.mClient, true); + } + } + + private void registerClientLocked(IMediaRouterClient client, + int pid, String packageName, int userId) { + final IBinder binder = client.asBinder(); + ClientRecord clientRecord = mAllClientRecords.get(binder); + if (clientRecord == null) { + boolean newUser = false; + UserRecord userRecord = mUserRecords.get(userId); + if (userRecord == null) { + userRecord = new UserRecord(userId); + newUser = true; + } + clientRecord = new ClientRecord(userRecord, client, pid, packageName); + try { + binder.linkToDeath(clientRecord, 0); + } catch (RemoteException ex) { + throw new RuntimeException("Media router client died prematurely.", ex); + } + + if (newUser) { + mUserRecords.put(userId, userRecord); + initializeUserLocked(userRecord); + } + + userRecord.mClientRecords.add(clientRecord); + mAllClientRecords.put(binder, clientRecord); + initializeClientLocked(clientRecord); + } + } + + private void unregisterClientLocked(IMediaRouterClient client, boolean died) { + ClientRecord clientRecord = mAllClientRecords.remove(client.asBinder()); + if (clientRecord != null) { + UserRecord userRecord = clientRecord.mUserRecord; + userRecord.mClientRecords.remove(clientRecord); + disposeClientLocked(clientRecord, died); + disposeUserIfNeededLocked(userRecord); // since client removed from user + } + } + + private MediaRouterClientState getStateLocked(IMediaRouterClient client) { + ClientRecord clientRecord = mAllClientRecords.get(client.asBinder()); + if (clientRecord != null) { + return clientRecord.mUserRecord.mState; + } + return null; + } + + private void setDiscoveryRequestLocked(IMediaRouterClient client, + int routeTypes, boolean activeScan) { + final IBinder binder = client.asBinder(); + ClientRecord clientRecord = mAllClientRecords.get(binder); + if (clientRecord != null) { + if (clientRecord.mRouteTypes != routeTypes + || clientRecord.mActiveScan != activeScan) { + if (DEBUG) { + Slog.d(TAG, clientRecord + ": Set discovery request, routeTypes=0x" + + Integer.toHexString(routeTypes) + ", activeScan=" + activeScan); + } + clientRecord.mRouteTypes = routeTypes; + clientRecord.mActiveScan = activeScan; + clientRecord.mUserRecord.mHandler.sendEmptyMessage( + UserHandler.MSG_UPDATE_DISCOVERY_REQUEST); + } + } + } + + private void setSelectedRouteLocked(IMediaRouterClient client, + String routeId, boolean explicit) { + ClientRecord clientRecord = mAllClientRecords.get(client.asBinder()); + if (clientRecord != null) { + final String oldRouteId = clientRecord.mSelectedRouteId; + if (!Objects.equal(routeId, oldRouteId)) { + if (DEBUG) { + Slog.d(TAG, clientRecord + ": Set selected route, routeId=" + routeId + + ", oldRouteId=" + oldRouteId + + ", explicit=" + explicit); + } + + clientRecord.mSelectedRouteId = routeId; + if (explicit) { + if (oldRouteId != null) { + clientRecord.mUserRecord.mHandler.obtainMessage( + UserHandler.MSG_UNSELECT_ROUTE, oldRouteId).sendToTarget(); + } + if (routeId != null) { + clientRecord.mUserRecord.mHandler.obtainMessage( + UserHandler.MSG_SELECT_ROUTE, routeId).sendToTarget(); + } + } + } + } + } + + private void requestSetVolumeLocked(IMediaRouterClient client, + String routeId, int volume) { + final IBinder binder = client.asBinder(); + ClientRecord clientRecord = mAllClientRecords.get(binder); + if (clientRecord != null) { + clientRecord.mUserRecord.mHandler.obtainMessage( + UserHandler.MSG_REQUEST_SET_VOLUME, volume, 0, routeId).sendToTarget(); + } + } + + private void requestUpdateVolumeLocked(IMediaRouterClient client, + String routeId, int direction) { + final IBinder binder = client.asBinder(); + ClientRecord clientRecord = mAllClientRecords.get(binder); + if (clientRecord != null) { + clientRecord.mUserRecord.mHandler.obtainMessage( + UserHandler.MSG_REQUEST_UPDATE_VOLUME, direction, 0, routeId).sendToTarget(); + } + } + + private void initializeUserLocked(UserRecord userRecord) { + if (DEBUG) { + Slog.d(TAG, userRecord + ": Initialized"); + } + if (userRecord.mUserId == mCurrentUserId) { + userRecord.mHandler.sendEmptyMessage(UserHandler.MSG_START); + } + } + + private void disposeUserIfNeededLocked(UserRecord userRecord) { + // If there are no records left and the user is no longer current then go ahead + // and purge the user record and all of its associated state. If the user is current + // then leave it alone since we might be connected to a route or want to query + // the same route information again soon. + if (userRecord.mUserId != mCurrentUserId + && userRecord.mClientRecords.isEmpty()) { + if (DEBUG) { + Slog.d(TAG, userRecord + ": Disposed"); + } + mUserRecords.remove(userRecord.mUserId); + // Note: User already stopped (by switchUser) so no need to send stop message here. + } + } + + private void initializeClientLocked(ClientRecord clientRecord) { + if (DEBUG) { + Slog.d(TAG, clientRecord + ": Registered"); + } + } + + private void disposeClientLocked(ClientRecord clientRecord, boolean died) { + if (DEBUG) { + if (died) { + Slog.d(TAG, clientRecord + ": Died!"); + } else { + Slog.d(TAG, clientRecord + ": Unregistered"); + } + } + if (clientRecord.mRouteTypes != 0 || clientRecord.mActiveScan) { + clientRecord.mUserRecord.mHandler.sendEmptyMessage( + UserHandler.MSG_UPDATE_DISCOVERY_REQUEST); + } + clientRecord.dispose(); + } + + 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; + } + + /** + * Information about a particular client of the media router. + * The contents of this object is guarded by mLock. + */ + final class ClientRecord implements DeathRecipient { + public final UserRecord mUserRecord; + public final IMediaRouterClient mClient; + public final int mPid; + public final String mPackageName; + + public int mRouteTypes; + public boolean mActiveScan; + public String mSelectedRouteId; + + public ClientRecord(UserRecord userRecord, IMediaRouterClient client, + int pid, String packageName) { + mUserRecord = userRecord; + mClient = client; + mPid = pid; + mPackageName = packageName; + } + + public void dispose() { + mClient.asBinder().unlinkToDeath(this, 0); + } + + @Override + public void binderDied() { + clientDied(this); + } + + public void dump(PrintWriter pw, String prefix) { + pw.println(prefix + this); + + final String indent = prefix + " "; + pw.println(indent + "mRouteTypes=0x" + Integer.toHexString(mRouteTypes)); + pw.println(indent + "mActiveScan=" + mActiveScan); + pw.println(indent + "mSelectedRouteId=" + mSelectedRouteId); + } + + @Override + public String toString() { + return "Client " + mPackageName + " (pid " + mPid + ")"; + } + } + + /** + * Information about a particular user. + * The contents of this object is guarded by mLock. + */ + final class UserRecord { + public final int mUserId; + public final ArrayList<ClientRecord> mClientRecords = new ArrayList<ClientRecord>(); + public final UserHandler mHandler; + public MediaRouterClientState mState; + + public UserRecord(int userId) { + mUserId = userId; + mHandler = new UserHandler(MediaRouterService.this, this); + } + + public void dump(final PrintWriter pw, String prefix) { + pw.println(prefix + this); + + final String indent = prefix + " "; + final int clientCount = mClientRecords.size(); + if (clientCount != 0) { + for (int i = 0; i < clientCount; i++) { + mClientRecords.get(i).dump(pw, indent); + } + } else { + pw.println(indent + "<no clients>"); + } + + if (!mHandler.runWithScissors(new Runnable() { + @Override + public void run() { + mHandler.dump(pw, indent); + } + }, 1000)) { + pw.println(indent + "<could not dump handler state>"); + } + } + + @Override + public String toString() { + return "User " + mUserId; + } + } + + /** + * Media router handler + * <p> + * Since remote display providers are designed to be single-threaded by nature, + * this class encapsulates all of the associated functionality and exports state + * to the service as it evolves. + * </p><p> + * One important task of this class is to keep track of the current globally selected + * route id for certain routes that have global effects, such as remote displays. + * Global route selections override local selections made within apps. The change + * is propagated to all apps so that they are all in sync. Synchronization works + * both ways. Whenever the globally selected route is explicitly unselected by any + * app, then it becomes unselected globally and all apps are informed. + * </p><p> + * This class is currently hardcoded to work with remote display providers but + * it is intended to be eventually extended to support more general route providers + * similar to the support library media router. + * </p> + */ + static final class UserHandler extends Handler + implements RemoteDisplayProviderWatcher.Callback, + RemoteDisplayProviderProxy.Callback { + public static final int MSG_START = 1; + public static final int MSG_STOP = 2; + public static final int MSG_UPDATE_DISCOVERY_REQUEST = 3; + public static final int MSG_SELECT_ROUTE = 4; + public static final int MSG_UNSELECT_ROUTE = 5; + public static final int MSG_REQUEST_SET_VOLUME = 6; + public static final int MSG_REQUEST_UPDATE_VOLUME = 7; + private static final int MSG_UPDATE_CLIENT_STATE = 8; + private static final int MSG_CONNECTION_TIMED_OUT = 9; + + private static final int TIMEOUT_REASON_NOT_AVAILABLE = 1; + private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTING = 2; + private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTED = 3; + + private final MediaRouterService mService; + private final UserRecord mUserRecord; + private final RemoteDisplayProviderWatcher mWatcher; + private final ArrayList<ProviderRecord> mProviderRecords = + new ArrayList<ProviderRecord>(); + private final ArrayList<IMediaRouterClient> mTempClients = + new ArrayList<IMediaRouterClient>(); + + private boolean mRunning; + private int mDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE; + private RouteRecord mGloballySelectedRouteRecord; + private int mConnectionTimeoutReason; + private long mConnectionTimeoutStartTime; + private boolean mClientStateUpdateScheduled; + + public UserHandler(MediaRouterService service, UserRecord userRecord) { + super(Looper.getMainLooper(), null, true); + mService = service; + mUserRecord = userRecord; + mWatcher = new RemoteDisplayProviderWatcher(service.mContext, this, + this, mUserRecord.mUserId); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_START: { + start(); + break; + } + case MSG_STOP: { + stop(); + break; + } + case MSG_UPDATE_DISCOVERY_REQUEST: { + updateDiscoveryRequest(); + break; + } + case MSG_SELECT_ROUTE: { + selectRoute((String)msg.obj); + break; + } + case MSG_UNSELECT_ROUTE: { + unselectRoute((String)msg.obj); + break; + } + case MSG_REQUEST_SET_VOLUME: { + requestSetVolume((String)msg.obj, msg.arg1); + break; + } + case MSG_REQUEST_UPDATE_VOLUME: { + requestUpdateVolume((String)msg.obj, msg.arg1); + break; + } + case MSG_UPDATE_CLIENT_STATE: { + updateClientState(); + break; + } + case MSG_CONNECTION_TIMED_OUT: { + connectionTimedOut(); + break; + } + } + } + + public void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "Handler"); + + final String indent = prefix + " "; + pw.println(indent + "mRunning=" + mRunning); + pw.println(indent + "mDiscoveryMode=" + mDiscoveryMode); + pw.println(indent + "mGloballySelectedRouteRecord=" + mGloballySelectedRouteRecord); + pw.println(indent + "mConnectionTimeoutReason=" + mConnectionTimeoutReason); + pw.println(indent + "mConnectionTimeoutStartTime=" + (mConnectionTimeoutReason != 0 ? + TimeUtils.formatUptime(mConnectionTimeoutStartTime) : "<n/a>")); + + mWatcher.dump(pw, prefix); + + final int providerCount = mProviderRecords.size(); + if (providerCount != 0) { + for (int i = 0; i < providerCount; i++) { + mProviderRecords.get(i).dump(pw, prefix); + } + } else { + pw.println(indent + "<no providers>"); + } + } + + private void start() { + if (!mRunning) { + mRunning = true; + mWatcher.start(); // also starts all providers + } + } + + private void stop() { + if (mRunning) { + mRunning = false; + unselectGloballySelectedRoute(); + mWatcher.stop(); // also stops all providers + } + } + + private void updateDiscoveryRequest() { + int routeTypes = 0; + boolean activeScan = false; + synchronized (mService.mLock) { + final int count = mUserRecord.mClientRecords.size(); + for (int i = 0; i < count; i++) { + ClientRecord clientRecord = mUserRecord.mClientRecords.get(i); + routeTypes |= clientRecord.mRouteTypes; + activeScan |= clientRecord.mActiveScan; + } + } + + final int newDiscoveryMode; + if ((routeTypes & (MediaRouter.ROUTE_TYPE_LIVE_VIDEO + | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) != 0) { + if (activeScan) { + newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_ACTIVE; + } else { + newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_PASSIVE; + } + } else { + newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE; + } + + if (mDiscoveryMode != newDiscoveryMode) { + mDiscoveryMode = newDiscoveryMode; + final int count = mProviderRecords.size(); + for (int i = 0; i < count; i++) { + mProviderRecords.get(i).getProvider().setDiscoveryMode(mDiscoveryMode); + } + } + } + + private void selectRoute(String routeId) { + if (routeId != null + && (mGloballySelectedRouteRecord == null + || !routeId.equals(mGloballySelectedRouteRecord.getUniqueId()))) { + RouteRecord routeRecord = findRouteRecord(routeId); + if (routeRecord != null) { + unselectGloballySelectedRoute(); + + Slog.i(TAG, "Selected global route:" + routeRecord); + mGloballySelectedRouteRecord = routeRecord; + checkGloballySelectedRouteState(); + routeRecord.getProvider().setSelectedDisplay(routeRecord.getDescriptorId()); + + scheduleUpdateClientState(); + } + } + } + + private void unselectRoute(String routeId) { + if (routeId != null + && mGloballySelectedRouteRecord != null + && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) { + unselectGloballySelectedRoute(); + } + } + + private void unselectGloballySelectedRoute() { + if (mGloballySelectedRouteRecord != null) { + Slog.i(TAG, "Unselected global route:" + mGloballySelectedRouteRecord); + mGloballySelectedRouteRecord.getProvider().setSelectedDisplay(null); + mGloballySelectedRouteRecord = null; + checkGloballySelectedRouteState(); + + scheduleUpdateClientState(); + } + } + + private void requestSetVolume(String routeId, int volume) { + if (mGloballySelectedRouteRecord != null + && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) { + mGloballySelectedRouteRecord.getProvider().setDisplayVolume(volume); + } + } + + private void requestUpdateVolume(String routeId, int direction) { + if (mGloballySelectedRouteRecord != null + && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) { + mGloballySelectedRouteRecord.getProvider().adjustDisplayVolume(direction); + } + } + + @Override + public void addProvider(RemoteDisplayProviderProxy provider) { + provider.setCallback(this); + provider.setDiscoveryMode(mDiscoveryMode); + provider.setSelectedDisplay(null); // just to be safe + + ProviderRecord providerRecord = new ProviderRecord(provider); + mProviderRecords.add(providerRecord); + providerRecord.updateDescriptor(provider.getDisplayState()); + + scheduleUpdateClientState(); + } + + @Override + public void removeProvider(RemoteDisplayProviderProxy provider) { + int index = findProviderRecord(provider); + if (index >= 0) { + ProviderRecord providerRecord = mProviderRecords.remove(index); + providerRecord.updateDescriptor(null); // mark routes invalid + provider.setCallback(null); + provider.setDiscoveryMode(RemoteDisplayState.DISCOVERY_MODE_NONE); + + checkGloballySelectedRouteState(); + scheduleUpdateClientState(); + } + } + + @Override + public void onDisplayStateChanged(RemoteDisplayProviderProxy provider, + RemoteDisplayState state) { + updateProvider(provider, state); + } + + private void updateProvider(RemoteDisplayProviderProxy provider, + RemoteDisplayState state) { + int index = findProviderRecord(provider); + if (index >= 0) { + ProviderRecord providerRecord = mProviderRecords.get(index); + if (providerRecord.updateDescriptor(state)) { + checkGloballySelectedRouteState(); + scheduleUpdateClientState(); + } + } + } + + /** + * This function is called whenever the state of the globally selected route + * may have changed. It checks the state and updates timeouts or unselects + * the route as appropriate. + */ + private void checkGloballySelectedRouteState() { + // Unschedule timeouts when the route is unselected. + if (mGloballySelectedRouteRecord == null) { + updateConnectionTimeout(0); + return; + } + + // Ensure that the route is still present and enabled. + if (!mGloballySelectedRouteRecord.isValid() + || !mGloballySelectedRouteRecord.isEnabled()) { + updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE); + return; + } + + // Check the route status. + switch (mGloballySelectedRouteRecord.getStatus()) { + case MediaRouter.RouteInfo.STATUS_NONE: + case MediaRouter.RouteInfo.STATUS_CONNECTED: + if (mConnectionTimeoutReason != 0) { + Slog.i(TAG, "Connected to global route: " + + mGloballySelectedRouteRecord); + } + updateConnectionTimeout(0); + break; + case MediaRouter.RouteInfo.STATUS_CONNECTING: + if (mConnectionTimeoutReason != 0) { + Slog.i(TAG, "Connecting to global route: " + + mGloballySelectedRouteRecord); + } + updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTED); + break; + case MediaRouter.RouteInfo.STATUS_SCANNING: + case MediaRouter.RouteInfo.STATUS_AVAILABLE: + updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTING); + break; + case MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE: + case MediaRouter.RouteInfo.STATUS_IN_USE: + default: + updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE); + break; + } + } + + private void updateConnectionTimeout(int reason) { + if (reason != mConnectionTimeoutReason) { + if (mConnectionTimeoutReason != 0) { + removeMessages(MSG_CONNECTION_TIMED_OUT); + } + mConnectionTimeoutReason = reason; + mConnectionTimeoutStartTime = SystemClock.uptimeMillis(); + switch (reason) { + case TIMEOUT_REASON_NOT_AVAILABLE: + // Route became unavailable. Unselect it immediately. + sendEmptyMessage(MSG_CONNECTION_TIMED_OUT); + break; + case TIMEOUT_REASON_WAITING_FOR_CONNECTING: + // Waiting for route to start connecting. + sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTING_TIMEOUT); + break; + case TIMEOUT_REASON_WAITING_FOR_CONNECTED: + // Waiting for route to complete connection. + sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTED_TIMEOUT); + break; + } + } + } + + private void connectionTimedOut() { + if (mConnectionTimeoutReason == 0 || mGloballySelectedRouteRecord == null) { + // Shouldn't get here. There must be a bug somewhere. + Log.wtf(TAG, "Handled connection timeout for no reason."); + return; + } + + switch (mConnectionTimeoutReason) { + case TIMEOUT_REASON_NOT_AVAILABLE: + Slog.i(TAG, "Global route no longer available: " + + mGloballySelectedRouteRecord); + break; + case TIMEOUT_REASON_WAITING_FOR_CONNECTING: + Slog.i(TAG, "Global route timed out while waiting for " + + "connection attempt to begin after " + + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime) + + " ms: " + mGloballySelectedRouteRecord); + break; + case TIMEOUT_REASON_WAITING_FOR_CONNECTED: + Slog.i(TAG, "Global route timed out while connecting after " + + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime) + + " ms: " + mGloballySelectedRouteRecord); + break; + } + mConnectionTimeoutReason = 0; + + unselectGloballySelectedRoute(); + } + + private void scheduleUpdateClientState() { + if (!mClientStateUpdateScheduled) { + mClientStateUpdateScheduled = true; + sendEmptyMessage(MSG_UPDATE_CLIENT_STATE); + } + } + + private void updateClientState() { + mClientStateUpdateScheduled = false; + + // Build a new client state. + MediaRouterClientState state = new MediaRouterClientState(); + state.globallySelectedRouteId = mGloballySelectedRouteRecord != null ? + mGloballySelectedRouteRecord.getUniqueId() : null; + final int providerCount = mProviderRecords.size(); + for (int i = 0; i < providerCount; i++) { + mProviderRecords.get(i).appendClientState(state); + } + + try { + synchronized (mService.mLock) { + // Update the UserRecord. + mUserRecord.mState = state; + + // Collect all clients. + final int count = mUserRecord.mClientRecords.size(); + for (int i = 0; i < count; i++) { + mTempClients.add(mUserRecord.mClientRecords.get(i).mClient); + } + } + + // Notify all clients (outside of the lock). + final int count = mTempClients.size(); + for (int i = 0; i < count; i++) { + try { + mTempClients.get(i).onStateChanged(); + } catch (RemoteException ex) { + // ignore errors, client probably died + } + } + } finally { + // Clear the list in preparation for the next time. + mTempClients.clear(); + } + } + + private int findProviderRecord(RemoteDisplayProviderProxy provider) { + final int count = mProviderRecords.size(); + for (int i = 0; i < count; i++) { + ProviderRecord record = mProviderRecords.get(i); + if (record.getProvider() == provider) { + return i; + } + } + return -1; + } + + private RouteRecord findRouteRecord(String uniqueId) { + final int count = mProviderRecords.size(); + for (int i = 0; i < count; i++) { + RouteRecord record = mProviderRecords.get(i).findRouteByUniqueId(uniqueId); + if (record != null) { + return record; + } + } + return null; + } + + static final class ProviderRecord { + private final RemoteDisplayProviderProxy mProvider; + private final String mUniquePrefix; + private final ArrayList<RouteRecord> mRoutes = new ArrayList<RouteRecord>(); + private RemoteDisplayState mDescriptor; + + public ProviderRecord(RemoteDisplayProviderProxy provider) { + mProvider = provider; + mUniquePrefix = provider.getFlattenedComponentName() + ":"; + } + + public RemoteDisplayProviderProxy getProvider() { + return mProvider; + } + + public String getUniquePrefix() { + return mUniquePrefix; + } + + public boolean updateDescriptor(RemoteDisplayState descriptor) { + boolean changed = false; + if (mDescriptor != descriptor) { + mDescriptor = descriptor; + + // Update all existing routes and reorder them to match + // the order of their descriptors. + int targetIndex = 0; + if (descriptor != null) { + if (descriptor.isValid()) { + final List<RemoteDisplayInfo> routeDescriptors = descriptor.displays; + final int routeCount = routeDescriptors.size(); + for (int i = 0; i < routeCount; i++) { + final RemoteDisplayInfo routeDescriptor = + routeDescriptors.get(i); + final String descriptorId = routeDescriptor.id; + final int sourceIndex = findRouteByDescriptorId(descriptorId); + if (sourceIndex < 0) { + // Add the route to the provider. + String uniqueId = assignRouteUniqueId(descriptorId); + RouteRecord route = + new RouteRecord(this, descriptorId, uniqueId); + mRoutes.add(targetIndex++, route); + route.updateDescriptor(routeDescriptor); + changed = true; + } else if (sourceIndex < targetIndex) { + // Ignore route with duplicate id. + Slog.w(TAG, "Ignoring route descriptor with duplicate id: " + + routeDescriptor); + } else { + // Reorder existing route within the list. + RouteRecord route = mRoutes.get(sourceIndex); + Collections.swap(mRoutes, sourceIndex, targetIndex++); + changed |= route.updateDescriptor(routeDescriptor); + } + } + } else { + Slog.w(TAG, "Ignoring invalid descriptor from media route provider: " + + mProvider.getFlattenedComponentName()); + } + } + + // Dispose all remaining routes that do not have matching descriptors. + for (int i = mRoutes.size() - 1; i >= targetIndex; i--) { + RouteRecord route = mRoutes.remove(i); + route.updateDescriptor(null); // mark route invalid + changed = true; + } + } + return changed; + } + + public void appendClientState(MediaRouterClientState state) { + final int routeCount = mRoutes.size(); + for (int i = 0; i < routeCount; i++) { + state.routes.add(mRoutes.get(i).getInfo()); + } + } + + public RouteRecord findRouteByUniqueId(String uniqueId) { + final int routeCount = mRoutes.size(); + for (int i = 0; i < routeCount; i++) { + RouteRecord route = mRoutes.get(i); + if (route.getUniqueId().equals(uniqueId)) { + return route; + } + } + return null; + } + + private int findRouteByDescriptorId(String descriptorId) { + final int routeCount = mRoutes.size(); + for (int i = 0; i < routeCount; i++) { + RouteRecord route = mRoutes.get(i); + if (route.getDescriptorId().equals(descriptorId)) { + return i; + } + } + return -1; + } + + public void dump(PrintWriter pw, String prefix) { + pw.println(prefix + this); + + final String indent = prefix + " "; + mProvider.dump(pw, indent); + + final int routeCount = mRoutes.size(); + if (routeCount != 0) { + for (int i = 0; i < routeCount; i++) { + mRoutes.get(i).dump(pw, indent); + } + } else { + pw.println(indent + "<no routes>"); + } + } + + @Override + public String toString() { + return "Provider " + mProvider.getFlattenedComponentName(); + } + + private String assignRouteUniqueId(String descriptorId) { + return mUniquePrefix + descriptorId; + } + } + + static final class RouteRecord { + private final ProviderRecord mProviderRecord; + private final String mDescriptorId; + private final MediaRouterClientState.RouteInfo mMutableInfo; + private MediaRouterClientState.RouteInfo mImmutableInfo; + private RemoteDisplayInfo mDescriptor; + + public RouteRecord(ProviderRecord providerRecord, + String descriptorId, String uniqueId) { + mProviderRecord = providerRecord; + mDescriptorId = descriptorId; + mMutableInfo = new MediaRouterClientState.RouteInfo(uniqueId); + } + + public RemoteDisplayProviderProxy getProvider() { + return mProviderRecord.getProvider(); + } + + public ProviderRecord getProviderRecord() { + return mProviderRecord; + } + + public String getDescriptorId() { + return mDescriptorId; + } + + public String getUniqueId() { + return mMutableInfo.id; + } + + public MediaRouterClientState.RouteInfo getInfo() { + if (mImmutableInfo == null) { + mImmutableInfo = new MediaRouterClientState.RouteInfo(mMutableInfo); + } + return mImmutableInfo; + } + + public boolean isValid() { + return mDescriptor != null; + } + + public boolean isEnabled() { + return mMutableInfo.enabled; + } + + public int getStatus() { + return mMutableInfo.statusCode; + } + + public boolean updateDescriptor(RemoteDisplayInfo descriptor) { + boolean changed = false; + if (mDescriptor != descriptor) { + mDescriptor = descriptor; + if (descriptor != null) { + final String name = computeName(descriptor); + if (!Objects.equal(mMutableInfo.name, name)) { + mMutableInfo.name = name; + changed = true; + } + final String description = computeDescription(descriptor); + if (!Objects.equal(mMutableInfo.description, description)) { + mMutableInfo.description = description; + changed = true; + } + final int supportedTypes = computeSupportedTypes(descriptor); + if (mMutableInfo.supportedTypes != supportedTypes) { + mMutableInfo.supportedTypes = supportedTypes; + changed = true; + } + final boolean enabled = computeEnabled(descriptor); + if (mMutableInfo.enabled != enabled) { + mMutableInfo.enabled = enabled; + changed = true; + } + final int statusCode = computeStatusCode(descriptor); + if (mMutableInfo.statusCode != statusCode) { + mMutableInfo.statusCode = statusCode; + changed = true; + } + final int playbackType = computePlaybackType(descriptor); + if (mMutableInfo.playbackType != playbackType) { + mMutableInfo.playbackType = playbackType; + changed = true; + } + final int playbackStream = computePlaybackStream(descriptor); + if (mMutableInfo.playbackStream != playbackStream) { + mMutableInfo.playbackStream = playbackStream; + changed = true; + } + final int volume = computeVolume(descriptor); + if (mMutableInfo.volume != volume) { + mMutableInfo.volume = volume; + changed = true; + } + final int volumeMax = computeVolumeMax(descriptor); + if (mMutableInfo.volumeMax != volumeMax) { + mMutableInfo.volumeMax = volumeMax; + changed = true; + } + final int volumeHandling = computeVolumeHandling(descriptor); + if (mMutableInfo.volumeHandling != volumeHandling) { + mMutableInfo.volumeHandling = volumeHandling; + changed = true; + } + final int presentationDisplayId = computePresentationDisplayId(descriptor); + if (mMutableInfo.presentationDisplayId != presentationDisplayId) { + mMutableInfo.presentationDisplayId = presentationDisplayId; + changed = true; + } + } + } + if (changed) { + mImmutableInfo = null; + } + return changed; + } + + public void dump(PrintWriter pw, String prefix) { + pw.println(prefix + this); + + final String indent = prefix + " "; + pw.println(indent + "mMutableInfo=" + mMutableInfo); + pw.println(indent + "mDescriptorId=" + mDescriptorId); + pw.println(indent + "mDescriptor=" + mDescriptor); + } + + @Override + public String toString() { + return "Route " + mMutableInfo.name + " (" + mMutableInfo.id + ")"; + } + + private static String computeName(RemoteDisplayInfo descriptor) { + // Note that isValid() already ensures the name is non-empty. + return descriptor.name; + } + + private static String computeDescription(RemoteDisplayInfo descriptor) { + final String description = descriptor.description; + return TextUtils.isEmpty(description) ? null : description; + } + + private static int computeSupportedTypes(RemoteDisplayInfo descriptor) { + return MediaRouter.ROUTE_TYPE_LIVE_AUDIO + | MediaRouter.ROUTE_TYPE_LIVE_VIDEO + | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY; + } + + private static boolean computeEnabled(RemoteDisplayInfo descriptor) { + switch (descriptor.status) { + case RemoteDisplayInfo.STATUS_CONNECTED: + case RemoteDisplayInfo.STATUS_CONNECTING: + case RemoteDisplayInfo.STATUS_AVAILABLE: + return true; + default: + return false; + } + } + + private static int computeStatusCode(RemoteDisplayInfo descriptor) { + switch (descriptor.status) { + case RemoteDisplayInfo.STATUS_NOT_AVAILABLE: + return MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE; + case RemoteDisplayInfo.STATUS_AVAILABLE: + return MediaRouter.RouteInfo.STATUS_AVAILABLE; + case RemoteDisplayInfo.STATUS_IN_USE: + return MediaRouter.RouteInfo.STATUS_IN_USE; + case RemoteDisplayInfo.STATUS_CONNECTING: + return MediaRouter.RouteInfo.STATUS_CONNECTING; + case RemoteDisplayInfo.STATUS_CONNECTED: + return MediaRouter.RouteInfo.STATUS_CONNECTED; + default: + return MediaRouter.RouteInfo.STATUS_NONE; + } + } + + private static int computePlaybackType(RemoteDisplayInfo descriptor) { + return MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE; + } + + private static int computePlaybackStream(RemoteDisplayInfo descriptor) { + return AudioSystem.STREAM_MUSIC; + } + + private static int computeVolume(RemoteDisplayInfo descriptor) { + final int volume = descriptor.volume; + final int volumeMax = descriptor.volumeMax; + if (volume < 0) { + return 0; + } else if (volume > volumeMax) { + return volumeMax; + } + return volume; + } + + private static int computeVolumeMax(RemoteDisplayInfo descriptor) { + final int volumeMax = descriptor.volumeMax; + return volumeMax > 0 ? volumeMax : 0; + } + + private static int computeVolumeHandling(RemoteDisplayInfo descriptor) { + final int volumeHandling = descriptor.volumeHandling; + switch (volumeHandling) { + case RemoteDisplayInfo.PLAYBACK_VOLUME_VARIABLE: + return MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE; + case RemoteDisplayInfo.PLAYBACK_VOLUME_FIXED: + default: + return MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED; + } + } + + private static int computePresentationDisplayId(RemoteDisplayInfo descriptor) { + // The MediaRouter class validates that the id corresponds to an extant + // presentation display. So all we do here is canonicalize the null case. + final int displayId = descriptor.presentationDisplayId; + return displayId < 0 ? -1 : displayId; + } + } + } +} diff --git a/services/java/com/android/server/media/RemoteDisplayProviderProxy.java b/services/java/com/android/server/media/RemoteDisplayProviderProxy.java new file mode 100644 index 0000000..b248ee0 --- /dev/null +++ b/services/java/com/android/server/media/RemoteDisplayProviderProxy.java @@ -0,0 +1,443 @@ +/* + * 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.media; + +import com.android.internal.util.Objects; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.IRemoteDisplayCallback; +import android.media.IRemoteDisplayProvider; +import android.media.RemoteDisplayState; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.IBinder.DeathRecipient; +import android.os.UserHandle; +import android.util.Log; +import android.util.Slog; + +import java.io.PrintWriter; +import java.lang.ref.WeakReference; + +/** + * Maintains a connection to a particular remote display provider service. + */ +final class RemoteDisplayProviderProxy implements ServiceConnection { + private static final String TAG = "RemoteDisplayProvider"; // max. 23 chars + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final Context mContext; + private final ComponentName mComponentName; + private final int mUserId; + private final Handler mHandler; + + private Callback mDisplayStateCallback; + + // Connection state + private boolean mRunning; + private boolean mBound; + private Connection mActiveConnection; + private boolean mConnectionReady; + + // Logical state + private int mDiscoveryMode; + private String mSelectedDisplayId; + private RemoteDisplayState mDisplayState; + private boolean mScheduledDisplayStateChangedCallback; + + public RemoteDisplayProviderProxy(Context context, ComponentName componentName, + int userId) { + mContext = context; + mComponentName = componentName; + mUserId = userId; + mHandler = new Handler(); + } + + public void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "Proxy"); + pw.println(prefix + " mUserId=" + mUserId); + pw.println(prefix + " mRunning=" + mRunning); + pw.println(prefix + " mBound=" + mBound); + pw.println(prefix + " mActiveConnection=" + mActiveConnection); + pw.println(prefix + " mConnectionReady=" + mConnectionReady); + pw.println(prefix + " mDiscoveryMode=" + mDiscoveryMode); + pw.println(prefix + " mSelectedDisplayId=" + mSelectedDisplayId); + pw.println(prefix + " mDisplayState=" + mDisplayState); + } + + public void setCallback(Callback callback) { + mDisplayStateCallback = callback; + } + + public RemoteDisplayState getDisplayState() { + return mDisplayState; + } + + public void setDiscoveryMode(int mode) { + if (mDiscoveryMode != mode) { + mDiscoveryMode = mode; + if (mConnectionReady) { + mActiveConnection.setDiscoveryMode(mode); + } + updateBinding(); + } + } + + public void setSelectedDisplay(String id) { + if (!Objects.equal(mSelectedDisplayId, id)) { + if (mConnectionReady && mSelectedDisplayId != null) { + mActiveConnection.disconnect(mSelectedDisplayId); + } + mSelectedDisplayId = id; + if (mConnectionReady && id != null) { + mActiveConnection.connect(id); + } + updateBinding(); + } + } + + public void setDisplayVolume(int volume) { + if (mConnectionReady && mSelectedDisplayId != null) { + mActiveConnection.setVolume(mSelectedDisplayId, volume); + } + } + + public void adjustDisplayVolume(int delta) { + if (mConnectionReady && mSelectedDisplayId != null) { + mActiveConnection.adjustVolume(mSelectedDisplayId, delta); + } + } + + public boolean hasComponentName(String packageName, String className) { + return mComponentName.getPackageName().equals(packageName) + && mComponentName.getClassName().equals(className); + } + + public String getFlattenedComponentName() { + return mComponentName.flattenToShortString(); + } + + public void start() { + if (!mRunning) { + if (DEBUG) { + Slog.d(TAG, this + ": Starting"); + } + + mRunning = true; + updateBinding(); + } + } + + public void stop() { + if (mRunning) { + if (DEBUG) { + Slog.d(TAG, this + ": Stopping"); + } + + mRunning = false; + updateBinding(); + } + } + + public void rebindIfDisconnected() { + if (mActiveConnection == null && shouldBind()) { + unbind(); + bind(); + } + } + + private void updateBinding() { + if (shouldBind()) { + bind(); + } else { + unbind(); + } + } + + private boolean shouldBind() { + if (mRunning) { + // Bind whenever there is a discovery request or selected display. + if (mDiscoveryMode != RemoteDisplayState.DISCOVERY_MODE_NONE + || mSelectedDisplayId != null) { + return true; + } + } + return false; + } + + private void bind() { + if (!mBound) { + if (DEBUG) { + Slog.d(TAG, this + ": Binding"); + } + + Intent service = new Intent(RemoteDisplayState.SERVICE_INTERFACE); + service.setComponent(mComponentName); + try { + mBound = mContext.bindServiceAsUser(service, this, Context.BIND_AUTO_CREATE, + new UserHandle(mUserId)); + if (!mBound && DEBUG) { + Slog.d(TAG, this + ": Bind failed"); + } + } catch (SecurityException ex) { + if (DEBUG) { + Slog.d(TAG, this + ": Bind failed", ex); + } + } + } + } + + private void unbind() { + if (mBound) { + if (DEBUG) { + Slog.d(TAG, this + ": Unbinding"); + } + + mBound = false; + disconnect(); + mContext.unbindService(this); + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (DEBUG) { + Slog.d(TAG, this + ": Connected"); + } + + if (mBound) { + disconnect(); + + IRemoteDisplayProvider provider = IRemoteDisplayProvider.Stub.asInterface(service); + if (provider != null) { + Connection connection = new Connection(provider); + if (connection.register()) { + mActiveConnection = connection; + } else { + if (DEBUG) { + Slog.d(TAG, this + ": Registration failed"); + } + } + } else { + Slog.e(TAG, this + ": Service returned invalid remote display provider binder"); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + if (DEBUG) { + Slog.d(TAG, this + ": Service disconnected"); + } + disconnect(); + } + + private void onConnectionReady(Connection connection) { + if (mActiveConnection == connection) { + mConnectionReady = true; + + if (mDiscoveryMode != RemoteDisplayState.DISCOVERY_MODE_NONE) { + mActiveConnection.setDiscoveryMode(mDiscoveryMode); + } + if (mSelectedDisplayId != null) { + mActiveConnection.connect(mSelectedDisplayId); + } + } + } + + private void onConnectionDied(Connection connection) { + if (mActiveConnection == connection) { + if (DEBUG) { + Slog.d(TAG, this + ": Service connection died"); + } + disconnect(); + } + } + + private void onDisplayStateChanged(Connection connection, RemoteDisplayState state) { + if (mActiveConnection == connection) { + if (DEBUG) { + Slog.d(TAG, this + ": State changed, state=" + state); + } + setDisplayState(state); + } + } + + private void disconnect() { + if (mActiveConnection != null) { + if (mSelectedDisplayId != null) { + mActiveConnection.disconnect(mSelectedDisplayId); + } + mConnectionReady = false; + mActiveConnection.dispose(); + mActiveConnection = null; + setDisplayState(null); + } + } + + private void setDisplayState(RemoteDisplayState state) { + if (!Objects.equal(mDisplayState, state)) { + mDisplayState = state; + if (!mScheduledDisplayStateChangedCallback) { + mScheduledDisplayStateChangedCallback = true; + mHandler.post(mDisplayStateChanged); + } + } + } + + @Override + public String toString() { + return "Service connection " + mComponentName.flattenToShortString(); + } + + private final Runnable mDisplayStateChanged = new Runnable() { + @Override + public void run() { + mScheduledDisplayStateChangedCallback = false; + if (mDisplayStateCallback != null) { + mDisplayStateCallback.onDisplayStateChanged( + RemoteDisplayProviderProxy.this, mDisplayState); + } + } + }; + + public interface Callback { + void onDisplayStateChanged(RemoteDisplayProviderProxy provider, RemoteDisplayState state); + } + + private final class Connection implements DeathRecipient { + private final IRemoteDisplayProvider mProvider; + private final ProviderCallback mCallback; + + public Connection(IRemoteDisplayProvider provider) { + mProvider = provider; + mCallback = new ProviderCallback(this); + } + + public boolean register() { + try { + mProvider.asBinder().linkToDeath(this, 0); + mProvider.setCallback(mCallback); + mHandler.post(new Runnable() { + @Override + public void run() { + onConnectionReady(Connection.this); + } + }); + return true; + } catch (RemoteException ex) { + binderDied(); + } + return false; + } + + public void dispose() { + mProvider.asBinder().unlinkToDeath(this, 0); + mCallback.dispose(); + } + + public void setDiscoveryMode(int mode) { + try { + mProvider.setDiscoveryMode(mode); + } catch (RemoteException ex) { + Slog.e(TAG, "Failed to deliver request to set discovery mode.", ex); + } + } + + public void connect(String id) { + try { + mProvider.connect(id); + } catch (RemoteException ex) { + Slog.e(TAG, "Failed to deliver request to connect to display.", ex); + } + } + + public void disconnect(String id) { + try { + mProvider.disconnect(id); + } catch (RemoteException ex) { + Slog.e(TAG, "Failed to deliver request to disconnect from display.", ex); + } + } + + public void setVolume(String id, int volume) { + try { + mProvider.setVolume(id, volume); + } catch (RemoteException ex) { + Slog.e(TAG, "Failed to deliver request to set display volume.", ex); + } + } + + public void adjustVolume(String id, int volume) { + try { + mProvider.adjustVolume(id, volume); + } catch (RemoteException ex) { + Slog.e(TAG, "Failed to deliver request to adjust display volume.", ex); + } + } + + @Override + public void binderDied() { + mHandler.post(new Runnable() { + @Override + public void run() { + onConnectionDied(Connection.this); + } + }); + } + + void postStateChanged(final RemoteDisplayState state) { + mHandler.post(new Runnable() { + @Override + public void run() { + onDisplayStateChanged(Connection.this, state); + } + }); + } + } + + /** + * Receives callbacks from the service. + * <p> + * This inner class is static and only retains a weak reference to the connection + * to prevent the client from being leaked in case the service is holding an + * active reference to the client's callback. + * </p> + */ + private static final class ProviderCallback extends IRemoteDisplayCallback.Stub { + private final WeakReference<Connection> mConnectionRef; + + public ProviderCallback(Connection connection) { + mConnectionRef = new WeakReference<Connection>(connection); + } + + public void dispose() { + mConnectionRef.clear(); + } + + @Override + public void onStateChanged(RemoteDisplayState state) throws RemoteException { + Connection connection = mConnectionRef.get(); + if (connection != null) { + connection.postStateChanged(state); + } + } + } +} diff --git a/services/java/com/android/server/media/RemoteDisplayProviderWatcher.java b/services/java/com/android/server/media/RemoteDisplayProviderWatcher.java new file mode 100644 index 0000000..f3a3c2f --- /dev/null +++ b/services/java/com/android/server/media/RemoteDisplayProviderWatcher.java @@ -0,0 +1,181 @@ +/* + * 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.media; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.media.RemoteDisplayState; +import android.os.Handler; +import android.os.UserHandle; +import android.util.Log; +import android.util.Slog; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; + +/** + * Watches for remote display provider services to be installed. + * Adds a provider to the media router for each registered service. + * + * @see RemoteDisplayProviderProxy + */ +public final class RemoteDisplayProviderWatcher { + private static final String TAG = "RemoteDisplayProvider"; // max. 23 chars + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final Context mContext; + private final Callback mCallback; + private final Handler mHandler; + private final int mUserId; + private final PackageManager mPackageManager; + + private final ArrayList<RemoteDisplayProviderProxy> mProviders = + new ArrayList<RemoteDisplayProviderProxy>(); + private boolean mRunning; + + public RemoteDisplayProviderWatcher(Context context, + Callback callback, Handler handler, int userId) { + mContext = context; + mCallback = callback; + mHandler = handler; + mUserId = userId; + mPackageManager = context.getPackageManager(); + } + + public void dump(PrintWriter pw, String prefix) { + pw.println(prefix + "Watcher"); + pw.println(prefix + " mUserId=" + mUserId); + pw.println(prefix + " mRunning=" + mRunning); + pw.println(prefix + " mProviders.size()=" + mProviders.size()); + } + + public void start() { + if (!mRunning) { + mRunning = true; + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_ADDED); + filter.addAction(Intent.ACTION_PACKAGE_REMOVED); + filter.addAction(Intent.ACTION_PACKAGE_CHANGED); + filter.addAction(Intent.ACTION_PACKAGE_REPLACED); + filter.addAction(Intent.ACTION_PACKAGE_RESTARTED); + filter.addDataScheme("package"); + mContext.registerReceiverAsUser(mScanPackagesReceiver, + new UserHandle(mUserId), filter, null, mHandler); + + // Scan packages. + // Also has the side-effect of restarting providers if needed. + mHandler.post(mScanPackagesRunnable); + } + } + + public void stop() { + if (mRunning) { + mRunning = false; + + mContext.unregisterReceiver(mScanPackagesReceiver); + mHandler.removeCallbacks(mScanPackagesRunnable); + + // Stop all providers. + for (int i = mProviders.size() - 1; i >= 0; i--) { + mProviders.get(i).stop(); + } + } + } + + private void scanPackages() { + if (!mRunning) { + return; + } + + // Add providers for all new services. + // Reorder the list so that providers left at the end will be the ones to remove. + int targetIndex = 0; + Intent intent = new Intent(RemoteDisplayState.SERVICE_INTERFACE); + for (ResolveInfo resolveInfo : mPackageManager.queryIntentServicesAsUser( + intent, 0, mUserId)) { + ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (serviceInfo != null) { + int sourceIndex = findProvider(serviceInfo.packageName, serviceInfo.name); + if (sourceIndex < 0) { + RemoteDisplayProviderProxy provider = + new RemoteDisplayProviderProxy(mContext, + new ComponentName(serviceInfo.packageName, serviceInfo.name), + mUserId); + provider.start(); + mProviders.add(targetIndex++, provider); + mCallback.addProvider(provider); + } else if (sourceIndex >= targetIndex) { + RemoteDisplayProviderProxy provider = mProviders.get(sourceIndex); + provider.start(); // restart the provider if needed + provider.rebindIfDisconnected(); + Collections.swap(mProviders, sourceIndex, targetIndex++); + } + } + } + + // Remove providers for missing services. + if (targetIndex < mProviders.size()) { + for (int i = mProviders.size() - 1; i >= targetIndex; i--) { + RemoteDisplayProviderProxy provider = mProviders.get(i); + mCallback.removeProvider(provider); + mProviders.remove(provider); + provider.stop(); + } + } + } + + private int findProvider(String packageName, String className) { + int count = mProviders.size(); + for (int i = 0; i < count; i++) { + RemoteDisplayProviderProxy provider = mProviders.get(i); + if (provider.hasComponentName(packageName, className)) { + return i; + } + } + return -1; + } + + private final BroadcastReceiver mScanPackagesReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (DEBUG) { + Slog.d(TAG, "Received package manager broadcast: " + intent); + } + scanPackages(); + } + }; + + private final Runnable mScanPackagesRunnable = new Runnable() { + @Override + public void run() { + scanPackages(); + } + }; + + public interface Callback { + void addProvider(RemoteDisplayProviderProxy provider); + void removeProvider(RemoteDisplayProviderProxy provider); + } +} diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java index 7f0d55f..194a0bb 100644 --- a/services/java/com/android/server/wm/WindowManagerService.java +++ b/services/java/com/android/server/wm/WindowManagerService.java @@ -575,10 +575,13 @@ public class WindowManagerService extends IWindowManager.Stub private boolean mUpdateRotation = false; boolean mWallpaperActionPending = false; - private static final int DISPLAY_CONTENT_UNKNOWN = 0; - private static final int DISPLAY_CONTENT_MIRROR = 1; - private static final int DISPLAY_CONTENT_UNIQUE = 2; - private int mDisplayHasContent = DISPLAY_CONTENT_UNKNOWN; + // Set to true when the display contains content to show the user. + // When false, the display manager may choose to mirror or blank the display. + boolean mDisplayHasContent = false; + + // Only set while traversing the default display based on its content. + // Affects the behavior of mirroring on secondary displays. + boolean mObscureApplicationContentOnSecondaryDisplays = false; } final LayoutFields mInnerFields = new LayoutFields(); @@ -8767,6 +8770,14 @@ public class WindowManagerService extends IWindowManager.Stub final WindowManager.LayoutParams attrs = w.mAttrs; final int attrFlags = attrs.flags; final boolean canBeSeen = w.isDisplayedLw(); + final boolean opaqueDrawn = canBeSeen && w.isOpaqueDrawn(); + + if (opaqueDrawn && w.isFullscreen(innerDw, innerDh)) { + // This window completely covers everything behind it, + // so we want to leave all of them as undimmed (for + // performance reasons). + mInnerFields.mObscured = true; + } if (w.mHasSurface) { if ((attrFlags&FLAG_KEEP_SCREEN_ON) != 0) { @@ -8795,22 +8806,24 @@ public class WindowManagerService extends IWindowManager.Stub } if (canBeSeen) { - if (type == TYPE_DREAM || type == TYPE_KEYGUARD) { - mInnerFields.mDisplayHasContent = LayoutFields.DISPLAY_CONTENT_MIRROR; - } else if (mInnerFields.mDisplayHasContent - == LayoutFields.DISPLAY_CONTENT_UNKNOWN) { - mInnerFields.mDisplayHasContent = LayoutFields.DISPLAY_CONTENT_UNIQUE; + // This function assumes that the contents of the default display are + // processed first before secondary displays. + if (w.mDisplayContent.isDefaultDisplay) { + // While a dream or keyguard is showing, obscure ordinary application + // content on secondary displays (by forcibly enabling mirroring unless + // there is other content we want to show) but still allow opaque + // keyguard dialogs to be shown. + if (type == TYPE_DREAM || type == TYPE_KEYGUARD) { + mInnerFields.mObscureApplicationContentOnSecondaryDisplays = true; + } + mInnerFields.mDisplayHasContent = true; + } else if (!mInnerFields.mObscureApplicationContentOnSecondaryDisplays + || (mInnerFields.mObscured && type == TYPE_KEYGUARD_DIALOG)) { + // Allow full screen keyguard presentation dialogs to be seen. + mInnerFields.mDisplayHasContent = true; } } } - - boolean opaqueDrawn = canBeSeen && w.isOpaqueDrawn(); - if (opaqueDrawn && w.isFullscreen(innerDw, innerDh)) { - // This window completely covers everything behind it, - // so we want to leave all of them as undimmed (for - // performance reasons). - mInnerFields.mObscured = true; - } } private void handleFlagDimBehind(WindowState w, int innerDw, int innerDh) { @@ -8888,7 +8901,7 @@ public class WindowManagerService extends IWindowManager.Stub mInnerFields.mScreenBrightness = -1; mInnerFields.mButtonBrightness = -1; mInnerFields.mUserActivityTimeout = -1; - mInnerFields.mDisplayHasContent = LayoutFields.DISPLAY_CONTENT_UNKNOWN; + mInnerFields.mObscureApplicationContentOnSecondaryDisplays = false; mTransactionSequence++; @@ -8923,10 +8936,8 @@ public class WindowManagerService extends IWindowManager.Stub final int innerDh = displayInfo.appHeight; final boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY); - // Reset for each display unless we are forcing mirroring. - if (mInnerFields.mDisplayHasContent != LayoutFields.DISPLAY_CONTENT_MIRROR) { - mInnerFields.mDisplayHasContent = LayoutFields.DISPLAY_CONTENT_UNKNOWN; - } + // Reset for each display. + mInnerFields.mDisplayHasContent = false; int repeats = 0; do { @@ -9135,20 +9146,8 @@ public class WindowManagerService extends IWindowManager.Stub updateResizingWindows(w); } - final boolean hasUniqueContent; - switch (mInnerFields.mDisplayHasContent) { - case LayoutFields.DISPLAY_CONTENT_MIRROR: - hasUniqueContent = isDefaultDisplay; - break; - case LayoutFields.DISPLAY_CONTENT_UNIQUE: - hasUniqueContent = true; - break; - case LayoutFields.DISPLAY_CONTENT_UNKNOWN: - default: - hasUniqueContent = false; - break; - } - mDisplayManagerService.setDisplayHasContent(displayId, hasUniqueContent, + mDisplayManagerService.setDisplayHasContent(displayId, + mInnerFields.mDisplayHasContent, true /* inTraversal, must call performTraversalInTrans... below */); getDisplayContentLocked(displayId).stopDimmingIfNeeded(); diff --git a/tests/RemoteDisplayProvider/Android.mk b/tests/RemoteDisplayProvider/Android.mk new file mode 100644 index 0000000..77e9815 --- /dev/null +++ b/tests/RemoteDisplayProvider/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 := RemoteDisplayProviderTest +LOCAL_MODULE_TAGS := tests +LOCAL_SDK_VERSION := current +LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_RESOURCE_DIR = $(LOCAL_PATH)/res +LOCAL_JAVA_LIBRARIES := com.android.media.remotedisplay +include $(BUILD_PACKAGE) diff --git a/tests/RemoteDisplayProvider/AndroidManifest.xml b/tests/RemoteDisplayProvider/AndroidManifest.xml new file mode 100644 index 0000000..e8e31da --- /dev/null +++ b/tests/RemoteDisplayProvider/AndroidManifest.xml @@ -0,0 +1,37 @@ +<?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.media.remotedisplay.test" > + + <uses-sdk android:minSdkVersion="19" /> + + <application android:label="@string/app_name" + android:icon="@drawable/ic_app"> + <uses-library android:name="com.android.media.remotedisplay" + android:required="true" /> + + <service android:name=".RemoteDisplayProviderService" + android:label="@string/app_name" + android:exported="true" + android:permission="android.permission.BIND_REMOTE_DISPLAY"> + <intent-filter> + <action android:name="com.android.media.remotedisplay.RemoteDisplayProvider"/> + </intent-filter> + </service> + + </application> +</manifest> diff --git a/tests/RemoteDisplayProvider/README b/tests/RemoteDisplayProvider/README new file mode 100644 index 0000000..8bf0130 --- /dev/null +++ b/tests/RemoteDisplayProvider/README @@ -0,0 +1,16 @@ +This directory contains sample code to test system integration with +remote display providers using the API declared by the +com.android.media.remotedisplay.jar library. + +--- DESCRIPTION --- + +The application registers a service that publishes a few different +remote display routes. Behavior can be controlled by modifying the +code. + +To exercise the provider, use System UI features for connecting to +wireless displays or launch an activity that uses the MediaRouter, +such as the PresentationWithMediaRouterActivity in ApiDemos. + +This code is mainly intended for development and not meant to be +used as an example implementation of a robust remote display provider. diff --git a/tests/RemoteDisplayProvider/res/drawable-hdpi/ic_app.png b/tests/RemoteDisplayProvider/res/drawable-hdpi/ic_app.png Binary files differnew file mode 100755 index 0000000..66a1984 --- /dev/null +++ b/tests/RemoteDisplayProvider/res/drawable-hdpi/ic_app.png diff --git a/tests/RemoteDisplayProvider/res/drawable-mdpi/ic_app.png b/tests/RemoteDisplayProvider/res/drawable-mdpi/ic_app.png Binary files differnew file mode 100644 index 0000000..5ae7701 --- /dev/null +++ b/tests/RemoteDisplayProvider/res/drawable-mdpi/ic_app.png diff --git a/tests/RemoteDisplayProvider/res/values/strings.xml b/tests/RemoteDisplayProvider/res/values/strings.xml new file mode 100644 index 0000000..dd82d2c --- /dev/null +++ b/tests/RemoteDisplayProvider/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">Remote Display Provider Test</string> +</resources> diff --git a/tests/RemoteDisplayProvider/src/com/android/media/remotedisplay/test/RemoteDisplayProviderService.java b/tests/RemoteDisplayProvider/src/com/android/media/remotedisplay/test/RemoteDisplayProviderService.java new file mode 100644 index 0000000..bf84631 --- /dev/null +++ b/tests/RemoteDisplayProvider/src/com/android/media/remotedisplay/test/RemoteDisplayProviderService.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.media.remotedisplay.test; + +import com.android.media.remotedisplay.RemoteDisplay; +import com.android.media.remotedisplay.RemoteDisplayProvider; + +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.util.Log; + +/** + * Remote display provider implementation that publishes working routes. + */ +public class RemoteDisplayProviderService extends Service { + private static final String TAG = "RemoteDisplayProviderTest"; + + private Provider mProvider; + + @Override + public IBinder onBind(Intent intent) { + if (intent.getAction().equals(RemoteDisplayProvider.SERVICE_INTERFACE)) { + if (mProvider == null) { + mProvider = new Provider(); + return mProvider.getBinder(); + } + } + return null; + } + + final class Provider extends RemoteDisplayProvider { + private RemoteDisplay mTestDisplay1; // variable volume + private RemoteDisplay mTestDisplay2; // fixed volume + private RemoteDisplay mTestDisplay3; // not available + private RemoteDisplay mTestDisplay4; // in use + private RemoteDisplay mTestDisplay5; // available but ignores request to connect + private RemoteDisplay mTestDisplay6; // available but never finishes connecting + private RemoteDisplay mTestDisplay7; // blinks in and out of existence + + private final Handler mHandler; + private boolean mBlinking; + + public Provider() { + super(RemoteDisplayProviderService.this); + mHandler = new Handler(getMainLooper()); + } + + @Override + public void onDiscoveryModeChanged(int mode) { + Log.d(TAG, "onDiscoveryModeChanged: mode=" + mode); + + if (mode != DISCOVERY_MODE_NONE) { + // When discovery begins, go find all of the routes. + if (mTestDisplay1 == null) { + mTestDisplay1 = new RemoteDisplay("testDisplay1", + "Test Display 1 (variable)"); + mTestDisplay1.setDescription("Variable volume"); + mTestDisplay1.setStatus(RemoteDisplay.STATUS_AVAILABLE); + mTestDisplay1.setVolume(10); + mTestDisplay1.setVolumeHandling(RemoteDisplay.PLAYBACK_VOLUME_VARIABLE); + mTestDisplay1.setVolumeMax(15); + addDisplay(mTestDisplay1); + } + if (mTestDisplay2 == null) { + mTestDisplay2 = new RemoteDisplay("testDisplay2", + "Test Display 2 (fixed)"); + mTestDisplay2.setDescription("Fixed volume"); + mTestDisplay2.setStatus(RemoteDisplay.STATUS_AVAILABLE); + addDisplay(mTestDisplay2); + } + if (mTestDisplay3 == null) { + mTestDisplay3 = new RemoteDisplay("testDisplay3", + "Test Display 3 (unavailable)"); + mTestDisplay3.setDescription("Always unavailable"); + mTestDisplay3.setStatus(RemoteDisplay.STATUS_NOT_AVAILABLE); + addDisplay(mTestDisplay3); + } + if (mTestDisplay4 == null) { + mTestDisplay4 = new RemoteDisplay("testDisplay4", + "Test Display 4 (in-use)"); + mTestDisplay4.setDescription("Always in-use"); + mTestDisplay4.setStatus(RemoteDisplay.STATUS_IN_USE); + addDisplay(mTestDisplay4); + } + if (mTestDisplay5 == null) { + mTestDisplay5 = new RemoteDisplay("testDisplay5", + "Test Display 5 (connect ignored)"); + mTestDisplay5.setDescription("Ignores connect"); + mTestDisplay5.setStatus(RemoteDisplay.STATUS_AVAILABLE); + addDisplay(mTestDisplay5); + } + if (mTestDisplay6 == null) { + mTestDisplay6 = new RemoteDisplay("testDisplay6", + "Test Display 6 (connect hangs)"); + mTestDisplay6.setDescription("Never finishes connecting"); + mTestDisplay6.setStatus(RemoteDisplay.STATUS_AVAILABLE); + addDisplay(mTestDisplay6); + } + } else { + // When discovery ends, go hide some of the routes we can't actually use. + // This isn't something a normal route provider would do though. + // The routes will usually stay published. + if (mTestDisplay3 != null) { + removeDisplay(mTestDisplay3); + mTestDisplay3 = null; + } + if (mTestDisplay4 != null) { + removeDisplay(mTestDisplay4); + mTestDisplay4 = null; + } + } + + // When active discovery is on, pretend there's a route that we can't quite + // reach that blinks in and out of existence. + if (mode == DISCOVERY_MODE_ACTIVE) { + if (!mBlinking) { + mBlinking = true; + mHandler.post(mBlink); + } + } else { + mBlinking = false; + } + } + + @Override + public void onConnect(final RemoteDisplay display) { + Log.d(TAG, "onConnect: display.getId()=" + display.getId()); + + if (display == mTestDisplay1 || display == mTestDisplay2) { + display.setStatus(RemoteDisplay.STATUS_CONNECTING); + mHandler.postDelayed(new Runnable() { + @Override + public void run() { + if ((display == mTestDisplay1 || display == mTestDisplay2) + && display.getStatus() == RemoteDisplay.STATUS_CONNECTING) { + display.setStatus(RemoteDisplay.STATUS_CONNECTED); + updateDisplay(display); + } + } + }, 2000); + updateDisplay(display); + } + if (display == mTestDisplay6 || display == mTestDisplay7) { + // never finishes connecting + display.setStatus(RemoteDisplay.STATUS_CONNECTING); + updateDisplay(display); + } + } + + @Override + public void onDisconnect(RemoteDisplay display) { + Log.d(TAG, "onDisconnect: display.getId()=" + display.getId()); + + if (display == mTestDisplay1 || display == mTestDisplay2 + || display == mTestDisplay6) { + display.setStatus(RemoteDisplay.STATUS_AVAILABLE); + updateDisplay(display); + } + } + + @Override + public void onSetVolume(RemoteDisplay display, int volume) { + Log.d(TAG, "onSetVolume: display.getId()=" + display.getId() + + ", volume=" + volume); + + if (display == mTestDisplay1) { + display.setVolume(Math.max(0, Math.min(display.getVolumeMax(), volume))); + updateDisplay(display); + } + } + + @Override + public void onAdjustVolume(RemoteDisplay display, int delta) { + Log.d(TAG, "onAdjustVolume: display.getId()=" + display.getId() + + ", delta=" + delta); + + if (display == mTestDisplay1) { + display.setVolume(Math.max(0, Math.min(display.getVolumeMax(), + display .getVolume() + delta))); + updateDisplay(display); + } + } + + @Override + public void addDisplay(RemoteDisplay display) { + Log.d(TAG, "addDisplay: display=" + display); + super.addDisplay(display); + } + + @Override + public void removeDisplay(RemoteDisplay display) { + Log.d(TAG, "removeDisplay: display=" + display); + super.removeDisplay(display); + } + + @Override + public void updateDisplay(RemoteDisplay display) { + Log.d(TAG, "updateDisplay: display=" + display); + super.updateDisplay(display); + } + + private final Runnable mBlink = new Runnable() { + @Override + public void run() { + if (mTestDisplay7 == null) { + if (mBlinking) { + mTestDisplay7 = new RemoteDisplay("testDisplay7", + "Test Display 7 (blinky)"); + mTestDisplay7.setDescription("Comes and goes but can't connect"); + mTestDisplay7.setStatus(RemoteDisplay.STATUS_AVAILABLE); + addDisplay(mTestDisplay7); + mHandler.postDelayed(this, 7000); + } + } else { + removeDisplay(mTestDisplay7); + mTestDisplay7 = null; + if (mBlinking) { + mHandler.postDelayed(this, 4000); + } + } + } + }; + } +} |
