Merge "Reset FontMetrics at each new measurement in BoringLayout" into nyc-dev
diff --git a/Android.mk b/Android.mk
index 024b2fd..1469c2c 100644
--- a/Android.mk
+++ b/Android.mk
@@ -251,10 +251,13 @@
core/java/android/print/IPrintDocumentAdapterObserver.aidl \
core/java/android/print/IPrintJobStateChangeListener.aidl \
core/java/android/print/IPrintServicesChangeListener.aidl \
+ core/java/android/printservice/recommendation/IRecommendationsChangeListener.aidl \
core/java/android/print/IPrintManager.aidl \
core/java/android/print/IPrintSpooler.aidl \
core/java/android/print/IPrintSpoolerCallbacks.aidl \
core/java/android/print/IPrintSpoolerClient.aidl \
+ core/java/android/printservice/recommendation/IRecommendationServiceCallbacks.aidl \
+ core/java/android/printservice/recommendation/IRecommendationService.aidl \
core/java/android/print/IWriteResultCallback.aidl \
core/java/android/printservice/IPrintService.aidl \
core/java/android/printservice/IPrintServiceClient.aidl \
@@ -565,6 +568,7 @@
frameworks/base/core/java/android/print/PrintJobInfo.aidl \
frameworks/base/core/java/android/print/PrinterInfo.aidl \
frameworks/base/core/java/android/print/PrintJobId.aidl \
+ frameworks/base/core/java/android/printservice/recommendation/RecommendationInfo.aidl \
frameworks/base/core/java/android/hardware/usb/UsbDevice.aidl \
frameworks/base/core/java/android/hardware/usb/UsbInterface.aidl \
frameworks/base/core/java/android/hardware/usb/UsbEndpoint.aidl \
diff --git a/api/current.txt b/api/current.txt
index 298f6c4..6a1bb02 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -3425,7 +3425,7 @@
method public boolean dispatchTouchEvent(android.view.MotionEvent);
method public boolean dispatchTrackballEvent(android.view.MotionEvent);
method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
- method public void enterPictureInPicture();
+ method public void enterPictureInPictureMode();
method public android.view.View findViewById(int);
method public void finish();
method public void finishActivity(int);
@@ -3465,15 +3465,16 @@
method public android.view.Window getWindow();
method public android.view.WindowManager getWindowManager();
method public boolean hasWindowFocus();
- method public boolean inMultiWindow();
- method public boolean inPictureInPicture();
method public void invalidateOptionsMenu();
method public boolean isChangingConfigurations();
method public final boolean isChild();
method public boolean isDestroyed();
method public boolean isFinishing();
method public boolean isImmersive();
+ method public boolean isInMultiWindowMode();
+ method public boolean isInPictureInPictureMode();
method public boolean isLocalVoiceInteractionSupported();
+ method public boolean isOverlayWithDecorCaptionEnabled();
method public boolean isTaskRoot();
method public boolean isVoiceInteraction();
method public boolean isVoiceInteractionRoot();
@@ -3520,7 +3521,7 @@
method public void onLowMemory();
method public boolean onMenuItemSelected(int, android.view.MenuItem);
method public boolean onMenuOpened(int, android.view.Menu);
- method public void onMultiWindowChanged(boolean);
+ method public void onMultiWindowModeChanged(boolean);
method public boolean onNavigateUp();
method public boolean onNavigateUpFromChild(android.app.Activity);
method protected void onNewIntent(android.content.Intent);
@@ -3528,7 +3529,7 @@
method public void onOptionsMenuClosed(android.view.Menu);
method public void onPanelClosed(int, android.view.Menu);
method protected void onPause();
- method public void onPictureInPictureChanged(boolean);
+ method public void onPictureInPictureModeChanged(boolean);
method protected void onPostCreate(android.os.Bundle);
method public void onPostCreate(android.os.Bundle, android.os.PersistableBundle);
method protected void onPostResume();
@@ -3566,7 +3567,6 @@
method public android.view.ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback, int);
method public void openContextMenu(android.view.View);
method public void openOptionsMenu();
- method public void overlayWithDecorCaption(boolean);
method public void overridePendingTransition(int, int);
method public void postponeEnterTransition();
method public void recreate();
@@ -3595,6 +3595,7 @@
method public void setImmersive(boolean);
method public void setIntent(android.content.Intent);
method public final void setMediaController(android.media.session.MediaController);
+ method public void setOverlayWithDecorCaptionEnabled(boolean);
method public final deprecated void setProgress(int);
method public final deprecated void setProgressBarIndeterminate(boolean);
method public final deprecated void setProgressBarIndeterminateVisibility(boolean);
@@ -4437,11 +4438,11 @@
method public void onInflate(android.content.Context, android.util.AttributeSet, android.os.Bundle);
method public deprecated void onInflate(android.app.Activity, android.util.AttributeSet, android.os.Bundle);
method public void onLowMemory();
- method public void onMultiWindowChanged(boolean);
+ method public void onMultiWindowModeChanged(boolean);
method public boolean onOptionsItemSelected(android.view.MenuItem);
method public void onOptionsMenuClosed(android.view.Menu);
method public void onPause();
- method public void onPictureInPictureChanged(boolean);
+ method public void onPictureInPictureModeChanged(boolean);
method public void onPrepareOptionsMenu(android.view.Menu);
method public void onRequestPermissionsResult(int, java.lang.String[], int[]);
method public void onResume();
@@ -4522,11 +4523,11 @@
method public void dispatchDestroy();
method public void dispatchDestroyView();
method public void dispatchLowMemory();
- method public void dispatchMultiWindowChanged(boolean);
+ method public void dispatchMultiWindowModeChanged(boolean);
method public boolean dispatchOptionsItemSelected(android.view.MenuItem);
method public void dispatchOptionsMenuClosed(android.view.Menu);
method public void dispatchPause();
- method public void dispatchPictureInPictureChanged(boolean);
+ method public void dispatchPictureInPictureModeChanged(boolean);
method public boolean dispatchPrepareOptionsMenu(android.view.Menu);
method public void dispatchResume();
method public void dispatchStart();
@@ -29129,6 +29130,7 @@
field public static final int RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY = 1; // 0x1
field public static final deprecated int SCREEN_BRIGHT_WAKE_LOCK = 10; // 0xa
field public static final deprecated int SCREEN_DIM_WAKE_LOCK = 6; // 0x6
+ field public static final int SUSTAINED_PERFORMANCE_WAKE_LOCK = 256; // 0x100
}
public final class PowerManager.WakeLock {
@@ -44648,6 +44650,7 @@
ctor public BaseInputConnection(android.view.View, boolean);
method public boolean beginBatchEdit();
method public boolean clearMetaKeyStates(int);
+ method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
@@ -44815,6 +44818,7 @@
public abstract interface InputConnection {
method public abstract boolean beginBatchEdit();
method public abstract boolean clearMetaKeyStates(int);
+ method public abstract void closeConnection();
method public abstract boolean commitCompletion(android.view.inputmethod.CompletionInfo);
method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public abstract boolean commitText(java.lang.CharSequence, int);
@@ -44847,6 +44851,7 @@
ctor public InputConnectionWrapper(android.view.inputmethod.InputConnection, boolean);
method public boolean beginBatchEdit();
method public boolean clearMetaKeyStates(int);
+ method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
diff --git a/api/system-current.txt b/api/system-current.txt
index e2816b4..6bf0717 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -42,6 +42,7 @@
field public static final java.lang.String BIND_MIDI_DEVICE_SERVICE = "android.permission.BIND_MIDI_DEVICE_SERVICE";
field public static final java.lang.String BIND_NFC_SERVICE = "android.permission.BIND_NFC_SERVICE";
field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE";
+ field public static final java.lang.String BIND_PRINT_RECOMMENDATION_SERVICE = "android.permission.BIND_PRINT_RECOMMENDATION_SERVICE";
field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE";
field public static final java.lang.String BIND_QUICK_SETTINGS_TILE = "android.permission.BIND_QUICK_SETTINGS_TILE";
field public static final java.lang.String BIND_REMOTEVIEWS = "android.permission.BIND_REMOTEVIEWS";
@@ -3540,7 +3541,7 @@
method public boolean dispatchTouchEvent(android.view.MotionEvent);
method public boolean dispatchTrackballEvent(android.view.MotionEvent);
method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
- method public void enterPictureInPicture();
+ method public void enterPictureInPictureMode();
method public android.view.View findViewById(int);
method public void finish();
method public void finishActivity(int);
@@ -3580,8 +3581,6 @@
method public android.view.Window getWindow();
method public android.view.WindowManager getWindowManager();
method public boolean hasWindowFocus();
- method public boolean inMultiWindow();
- method public boolean inPictureInPicture();
method public void invalidateOptionsMenu();
method public boolean isBackgroundVisibleBehind();
method public boolean isChangingConfigurations();
@@ -3589,7 +3588,10 @@
method public boolean isDestroyed();
method public boolean isFinishing();
method public boolean isImmersive();
+ method public boolean isInMultiWindowMode();
+ method public boolean isInPictureInPictureMode();
method public boolean isLocalVoiceInteractionSupported();
+ method public boolean isOverlayWithDecorCaptionEnabled();
method public boolean isTaskRoot();
method public boolean isVoiceInteraction();
method public boolean isVoiceInteractionRoot();
@@ -3637,7 +3639,7 @@
method public void onLowMemory();
method public boolean onMenuItemSelected(int, android.view.MenuItem);
method public boolean onMenuOpened(int, android.view.Menu);
- method public void onMultiWindowChanged(boolean);
+ method public void onMultiWindowModeChanged(boolean);
method public boolean onNavigateUp();
method public boolean onNavigateUpFromChild(android.app.Activity);
method protected void onNewIntent(android.content.Intent);
@@ -3645,7 +3647,7 @@
method public void onOptionsMenuClosed(android.view.Menu);
method public void onPanelClosed(int, android.view.Menu);
method protected void onPause();
- method public void onPictureInPictureChanged(boolean);
+ method public void onPictureInPictureModeChanged(boolean);
method protected void onPostCreate(android.os.Bundle);
method public void onPostCreate(android.os.Bundle, android.os.PersistableBundle);
method protected void onPostResume();
@@ -3683,7 +3685,6 @@
method public android.view.ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback, int);
method public void openContextMenu(android.view.View);
method public void openOptionsMenu();
- method public void overlayWithDecorCaption(boolean);
method public void overridePendingTransition(int, int);
method public void postponeEnterTransition();
method public void recreate();
@@ -3712,6 +3713,7 @@
method public void setImmersive(boolean);
method public void setIntent(android.content.Intent);
method public final void setMediaController(android.media.session.MediaController);
+ method public void setOverlayWithDecorCaptionEnabled(boolean);
method public final deprecated void setProgress(int);
method public final deprecated void setProgressBarIndeterminate(boolean);
method public final deprecated void setProgressBarIndeterminateVisibility(boolean);
@@ -4569,11 +4571,11 @@
method public void onInflate(android.content.Context, android.util.AttributeSet, android.os.Bundle);
method public deprecated void onInflate(android.app.Activity, android.util.AttributeSet, android.os.Bundle);
method public void onLowMemory();
- method public void onMultiWindowChanged(boolean);
+ method public void onMultiWindowModeChanged(boolean);
method public boolean onOptionsItemSelected(android.view.MenuItem);
method public void onOptionsMenuClosed(android.view.Menu);
method public void onPause();
- method public void onPictureInPictureChanged(boolean);
+ method public void onPictureInPictureModeChanged(boolean);
method public void onPrepareOptionsMenu(android.view.Menu);
method public void onRequestPermissionsResult(int, java.lang.String[], int[]);
method public void onResume();
@@ -4654,11 +4656,11 @@
method public void dispatchDestroy();
method public void dispatchDestroyView();
method public void dispatchLowMemory();
- method public void dispatchMultiWindowChanged(boolean);
+ method public void dispatchMultiWindowModeChanged(boolean);
method public boolean dispatchOptionsItemSelected(android.view.MenuItem);
method public void dispatchOptionsMenuClosed(android.view.Menu);
method public void dispatchPause();
- method public void dispatchPictureInPictureChanged(boolean);
+ method public void dispatchPictureInPictureModeChanged(boolean);
method public boolean dispatchPrepareOptionsMenu(android.view.Menu);
method public void dispatchResume();
method public void dispatchStart();
@@ -26819,7 +26821,9 @@
method public void configureWifiChange(android.net.wifi.WifiScanner.WifiChangeSettings);
method public boolean getScanResults();
method public void startBackgroundScan(android.net.wifi.WifiScanner.ScanSettings, android.net.wifi.WifiScanner.ScanListener);
+ method public void startBackgroundScan(android.net.wifi.WifiScanner.ScanSettings, android.net.wifi.WifiScanner.ScanListener, android.os.WorkSource);
method public void startScan(android.net.wifi.WifiScanner.ScanSettings, android.net.wifi.WifiScanner.ScanListener);
+ method public void startScan(android.net.wifi.WifiScanner.ScanSettings, android.net.wifi.WifiScanner.ScanListener, android.os.WorkSource);
method public void startTrackingBssids(android.net.wifi.WifiScanner.BssidInfo[], int, android.net.wifi.WifiScanner.BssidListener);
method public void startTrackingWifiChange(android.net.wifi.WifiScanner.WifiChangeListener);
method public void stopBackgroundScan(android.net.wifi.WifiScanner.ScanListener);
@@ -31374,6 +31378,7 @@
field public static final deprecated int SCREEN_BRIGHT_WAKE_LOCK = 10; // 0xa
field public static final deprecated int SCREEN_DIM_WAKE_LOCK = 6; // 0x6
field public static final int USER_ACTIVITY_EVENT_ACCESSIBILITY = 3; // 0x3
+ field public static final int SUSTAINED_PERFORMANCE_WAKE_LOCK = 256; // 0x100
field public static final int USER_ACTIVITY_EVENT_BUTTON = 1; // 0x1
field public static final int USER_ACTIVITY_EVENT_OTHER = 0; // 0x0
field public static final int USER_ACTIVITY_EVENT_TOUCH = 2; // 0x2
@@ -32698,6 +32703,30 @@
}
+package android.printservice.recommendation {
+
+ public final class RecommendationInfo implements android.os.Parcelable {
+ ctor public RecommendationInfo(java.lang.CharSequence, java.lang.CharSequence, int, boolean);
+ method public int describeContents();
+ method public java.lang.CharSequence getName();
+ method public int getNumDiscoveredPrinters();
+ method public java.lang.CharSequence getPackageName();
+ method public boolean recommendsMultiVendorService();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.printservice.recommendation.RecommendationInfo> CREATOR;
+ }
+
+ public abstract class RecommendationService extends android.app.Service {
+ ctor public RecommendationService();
+ method public final android.os.IBinder onBind(android.content.Intent);
+ method public abstract void onConnected();
+ method public abstract void onDisconnected();
+ method public final boolean onUnbind(android.content.Intent);
+ method public final void updateRecommendations(java.util.List<android.printservice.recommendation.RecommendationInfo>);
+ }
+
+}
+
package android.provider {
public final class AlarmClock {
@@ -47377,6 +47406,7 @@
ctor public BaseInputConnection(android.view.View, boolean);
method public boolean beginBatchEdit();
method public boolean clearMetaKeyStates(int);
+ method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
@@ -47544,6 +47574,7 @@
public abstract interface InputConnection {
method public abstract boolean beginBatchEdit();
method public abstract boolean clearMetaKeyStates(int);
+ method public abstract void closeConnection();
method public abstract boolean commitCompletion(android.view.inputmethod.CompletionInfo);
method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public abstract boolean commitText(java.lang.CharSequence, int);
@@ -47576,6 +47607,7 @@
ctor public InputConnectionWrapper(android.view.inputmethod.InputConnection, boolean);
method public boolean beginBatchEdit();
method public boolean clearMetaKeyStates(int);
+ method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
diff --git a/api/test-current.txt b/api/test-current.txt
index 2a9f452..3d03d5b 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -3425,7 +3425,7 @@
method public boolean dispatchTouchEvent(android.view.MotionEvent);
method public boolean dispatchTrackballEvent(android.view.MotionEvent);
method public void dump(java.lang.String, java.io.FileDescriptor, java.io.PrintWriter, java.lang.String[]);
- method public void enterPictureInPicture();
+ method public void enterPictureInPictureMode();
method public android.view.View findViewById(int);
method public void finish();
method public void finishActivity(int);
@@ -3465,15 +3465,16 @@
method public android.view.Window getWindow();
method public android.view.WindowManager getWindowManager();
method public boolean hasWindowFocus();
- method public boolean inMultiWindow();
- method public boolean inPictureInPicture();
method public void invalidateOptionsMenu();
method public boolean isChangingConfigurations();
method public final boolean isChild();
method public boolean isDestroyed();
method public boolean isFinishing();
method public boolean isImmersive();
+ method public boolean isInMultiWindowMode();
+ method public boolean isInPictureInPictureMode();
method public boolean isLocalVoiceInteractionSupported();
+ method public boolean isOverlayWithDecorCaptionEnabled();
method public boolean isTaskRoot();
method public boolean isVoiceInteraction();
method public boolean isVoiceInteractionRoot();
@@ -3520,7 +3521,7 @@
method public void onLowMemory();
method public boolean onMenuItemSelected(int, android.view.MenuItem);
method public boolean onMenuOpened(int, android.view.Menu);
- method public void onMultiWindowChanged(boolean);
+ method public void onMultiWindowModeChanged(boolean);
method public boolean onNavigateUp();
method public boolean onNavigateUpFromChild(android.app.Activity);
method protected void onNewIntent(android.content.Intent);
@@ -3528,7 +3529,7 @@
method public void onOptionsMenuClosed(android.view.Menu);
method public void onPanelClosed(int, android.view.Menu);
method protected void onPause();
- method public void onPictureInPictureChanged(boolean);
+ method public void onPictureInPictureModeChanged(boolean);
method protected void onPostCreate(android.os.Bundle);
method public void onPostCreate(android.os.Bundle, android.os.PersistableBundle);
method protected void onPostResume();
@@ -3566,7 +3567,6 @@
method public android.view.ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback, int);
method public void openContextMenu(android.view.View);
method public void openOptionsMenu();
- method public void overlayWithDecorCaption(boolean);
method public void overridePendingTransition(int, int);
method public void postponeEnterTransition();
method public void recreate();
@@ -3595,6 +3595,7 @@
method public void setImmersive(boolean);
method public void setIntent(android.content.Intent);
method public final void setMediaController(android.media.session.MediaController);
+ method public void setOverlayWithDecorCaptionEnabled(boolean);
method public final deprecated void setProgress(int);
method public final deprecated void setProgressBarIndeterminate(boolean);
method public final deprecated void setProgressBarIndeterminateVisibility(boolean);
@@ -4437,11 +4438,11 @@
method public void onInflate(android.content.Context, android.util.AttributeSet, android.os.Bundle);
method public deprecated void onInflate(android.app.Activity, android.util.AttributeSet, android.os.Bundle);
method public void onLowMemory();
- method public void onMultiWindowChanged(boolean);
+ method public void onMultiWindowModeChanged(boolean);
method public boolean onOptionsItemSelected(android.view.MenuItem);
method public void onOptionsMenuClosed(android.view.Menu);
method public void onPause();
- method public void onPictureInPictureChanged(boolean);
+ method public void onPictureInPictureModeChanged(boolean);
method public void onPrepareOptionsMenu(android.view.Menu);
method public void onRequestPermissionsResult(int, java.lang.String[], int[]);
method public void onResume();
@@ -4522,11 +4523,11 @@
method public void dispatchDestroy();
method public void dispatchDestroyView();
method public void dispatchLowMemory();
- method public void dispatchMultiWindowChanged(boolean);
+ method public void dispatchMultiWindowModeChanged(boolean);
method public boolean dispatchOptionsItemSelected(android.view.MenuItem);
method public void dispatchOptionsMenuClosed(android.view.Menu);
method public void dispatchPause();
- method public void dispatchPictureInPictureChanged(boolean);
+ method public void dispatchPictureInPictureModeChanged(boolean);
method public boolean dispatchPrepareOptionsMenu(android.view.Menu);
method public void dispatchResume();
method public void dispatchStart();
@@ -29194,6 +29195,7 @@
field public static final int RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY = 1; // 0x1
field public static final deprecated int SCREEN_BRIGHT_WAKE_LOCK = 10; // 0xa
field public static final deprecated int SCREEN_DIM_WAKE_LOCK = 6; // 0x6
+ field public static final int SUSTAINED_PERFORMANCE_WAKE_LOCK = 256; // 0x100
}
public final class PowerManager.WakeLock {
@@ -44722,6 +44724,7 @@
ctor public BaseInputConnection(android.view.View, boolean);
method public boolean beginBatchEdit();
method public boolean clearMetaKeyStates(int);
+ method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
@@ -44889,6 +44892,7 @@
public abstract interface InputConnection {
method public abstract boolean beginBatchEdit();
method public abstract boolean clearMetaKeyStates(int);
+ method public abstract void closeConnection();
method public abstract boolean commitCompletion(android.view.inputmethod.CompletionInfo);
method public abstract boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public abstract boolean commitText(java.lang.CharSequence, int);
@@ -44921,6 +44925,7 @@
ctor public InputConnectionWrapper(android.view.inputmethod.InputConnection, boolean);
method public boolean beginBatchEdit();
method public boolean clearMetaKeyStates(int);
+ method public void closeConnection();
method public boolean commitCompletion(android.view.inputmethod.CompletionInfo);
method public boolean commitCorrection(android.view.inputmethod.CorrectionInfo);
method public boolean commitText(java.lang.CharSequence, int);
diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java
index 4025553..d44a1df 100644
--- a/cmds/pm/src/com/android/commands/pm/Pm.java
+++ b/cmds/pm/src/com/android/commands/pm/Pm.java
@@ -930,7 +930,7 @@
// In non-split user mode, userId can only be SYSTEM
int parentUserId = userId >= 0 ? userId : UserHandle.USER_SYSTEM;
info = mUm.createRestrictedProfile(name, parentUserId);
- mAm.addSharedAccountsFromParentUser(userId, parentUserId);
+ mAm.addSharedAccountsFromParentUser(parentUserId, userId);
} else if (userId < 0) {
info = mUm.createUser(name, flags);
} else {
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 0d387e6..0410a6e 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -51,12 +51,7 @@
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
-import android.graphics.drawable.InsetDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.graphics.drawable.ShapeDrawable;
import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.media.session.MediaController;
@@ -121,7 +116,6 @@
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.app.ToolbarActionBar;
import com.android.internal.app.WindowDecorActionBar;
-import com.android.internal.policy.DecorView;
import com.android.internal.policy.PhoneWindow;
import java.io.FileDescriptor;
@@ -1855,15 +1849,15 @@
* visa-versa.
* @see android.R.attr#resizeableActivity
*
- * @param inMultiWindow True if the activity is in multi-window mode.
+ * @param isInMultiWindowMode True if the activity is in multi-window mode.
*/
@CallSuper
- public void onMultiWindowChanged(boolean inMultiWindow) {
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
if (DEBUG_LIFECYCLE) Slog.v(TAG,
- "onMultiWindowChanged " + this + ": " + inMultiWindow);
- mFragments.dispatchMultiWindowChanged(inMultiWindow);
+ "onMultiWindowModeChanged " + this + ": " + isInMultiWindowMode);
+ mFragments.dispatchMultiWindowModeChanged(isInMultiWindowMode);
if (mWindow != null) {
- mWindow.onMultiWindowChanged();
+ mWindow.onMultiWindowModeChanged();
}
}
@@ -1873,9 +1867,9 @@
*
* @return True if the activity is in multi-window mode.
*/
- public boolean inMultiWindow() {
+ public boolean isInMultiWindowMode() {
try {
- return ActivityManagerNative.getDefault().inMultiWindow(mToken);
+ return ActivityManagerNative.getDefault().isInMultiWindowMode(mToken);
} catch (RemoteException e) {
}
return false;
@@ -1885,13 +1879,13 @@
* Called by the system when the activity changes to and from picture-in-picture mode.
* @see android.R.attr#supportsPictureInPicture
*
- * @param inPictureInPicture True if the activity is in picture-in-picture mode.
+ * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode.
*/
@CallSuper
- public void onPictureInPictureChanged(boolean inPictureInPicture) {
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
if (DEBUG_LIFECYCLE) Slog.v(TAG,
- "onPictureInPictureChanged " + this + ": " + inPictureInPicture);
- mFragments.dispatchPictureInPictureChanged(inPictureInPicture);
+ "onPictureInPictureModeChanged " + this + ": " + isInPictureInPictureMode);
+ mFragments.dispatchPictureInPictureModeChanged(isInPictureInPictureMode);
}
/**
@@ -1900,9 +1894,9 @@
*
* @return True if the activity is in picture-in-picture mode.
*/
- public boolean inPictureInPicture() {
+ public boolean isInPictureInPictureMode() {
try {
- return ActivityManagerNative.getDefault().inPictureInPicture(mToken);
+ return ActivityManagerNative.getDefault().isInPictureInPictureMode(mToken);
} catch (RemoteException e) {
}
return false;
@@ -1912,9 +1906,9 @@
* Puts the activity in picture-in-picture mode.
* @see android.R.attr#supportsPictureInPicture
*/
- public void enterPictureInPicture() {
+ public void enterPictureInPictureMode() {
try {
- ActivityManagerNative.getDefault().enterPictureInPicture(mToken);
+ ActivityManagerNative.getDefault().enterPictureInPictureMode(mToken);
} catch (RemoteException e) {
}
}
@@ -6915,14 +6909,25 @@
}
/**
+ * Check whether the caption on freeform windows is displayed directly on the content.
+ *
+ * @return True if caption is displayed on content, false if it pushes the content down.
+ *
+ * @see {@link #setOverlayWithDecorCaptionEnabled(boolean)}
+ */
+ public boolean isOverlayWithDecorCaptionEnabled() {
+ return mWindow.isOverlayWithDecorCaptionEnabled();
+ }
+
+ /**
* Set whether the caption should displayed directly on the content rather than push it down.
*
* This affects only freeform windows since they display the caption and only the main
* window of the activity. The caption is used to drag the window around and also shows
* maximize and close action buttons.
*/
- public void overlayWithDecorCaption(boolean overlay) {
- mWindow.setOverlayDecorCaption(overlay);
+ public void setOverlayWithDecorCaptionEnabled(boolean enabled) {
+ mWindow.setOverlayWithDecorCaptionEnabled(enabled);
}
/**
diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java
index 4bf48a3..33621852 100644
--- a/core/java/android/app/ActivityManagerNative.java
+++ b/core/java/android/app/ActivityManagerNative.java
@@ -2890,7 +2890,7 @@
case IN_MULTI_WINDOW_TRANSACTION: {
data.enforceInterface(IActivityManager.descriptor);
final IBinder token = data.readStrongBinder();
- final boolean inMultiWindow = inMultiWindow(token);
+ final boolean inMultiWindow = isInMultiWindowMode(token);
reply.writeNoException();
reply.writeInt(inMultiWindow ? 1 : 0);
return true;
@@ -2898,7 +2898,7 @@
case IN_PICTURE_IN_PICTURE_TRANSACTION: {
data.enforceInterface(IActivityManager.descriptor);
final IBinder token = data.readStrongBinder();
- final boolean inPip = inPictureInPicture(token);
+ final boolean inPip = isInPictureInPictureMode(token);
reply.writeNoException();
reply.writeInt(inPip ? 1 : 0);
return true;
@@ -2906,7 +2906,7 @@
case ENTER_PICTURE_IN_PICTURE_TRANSACTION: {
data.enforceInterface(IActivityManager.descriptor);
final IBinder token = data.readStrongBinder();
- enterPictureInPicture(token);
+ enterPictureInPictureMode(token);
reply.writeNoException();
return true;
}
@@ -6837,7 +6837,7 @@
}
@Override
- public boolean inMultiWindow(IBinder token) throws RemoteException {
+ public boolean isInMultiWindowMode(IBinder token) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IActivityManager.descriptor);
@@ -6851,7 +6851,7 @@
}
@Override
- public boolean inPictureInPicture(IBinder token) throws RemoteException {
+ public boolean isInPictureInPictureMode(IBinder token) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IActivityManager.descriptor);
@@ -6865,7 +6865,7 @@
}
@Override
- public void enterPictureInPicture(IBinder token) throws RemoteException {
+ public void enterPictureInPictureMode(IBinder token) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IActivityManager.descriptor);
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index dbc6c84..5b94696 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1284,15 +1284,15 @@
}
@Override
- public void scheduleMultiWindowChanged(IBinder token, boolean inMultiWindow)
+ public void scheduleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode)
throws RemoteException {
- sendMessage(H.MULTI_WINDOW_CHANGED, token, inMultiWindow ? 1 : 0);
+ sendMessage(H.MULTI_WINDOW_MODE_CHANGED, token, isInMultiWindowMode ? 1 : 0);
}
@Override
- public void schedulePictureInPictureChanged(IBinder token, boolean inPip)
+ public void schedulePictureInPictureModeChanged(IBinder token, boolean isInPipMode)
throws RemoteException {
- sendMessage(H.PICTURE_IN_PICTURE_CHANGED, token, inPip ? 1 : 0);
+ sendMessage(H.PICTURE_IN_PICTURE_MODE_CHANGED, token, isInPipMode ? 1 : 0);
}
@Override
@@ -1364,8 +1364,8 @@
public static final int ENTER_ANIMATION_COMPLETE = 149;
public static final int START_BINDER_TRACKING = 150;
public static final int STOP_BINDER_TRACKING_AND_DUMP = 151;
- public static final int MULTI_WINDOW_CHANGED = 152;
- public static final int PICTURE_IN_PICTURE_CHANGED = 153;
+ public static final int MULTI_WINDOW_MODE_CHANGED = 152;
+ public static final int PICTURE_IN_PICTURE_MODE_CHANGED = 153;
public static final int LOCAL_VOICE_INTERACTION_STARTED = 154;
String codeToString(int code) {
@@ -1420,8 +1420,8 @@
case CANCEL_VISIBLE_BEHIND: return "CANCEL_VISIBLE_BEHIND";
case BACKGROUND_VISIBLE_BEHIND_CHANGED: return "BACKGROUND_VISIBLE_BEHIND_CHANGED";
case ENTER_ANIMATION_COMPLETE: return "ENTER_ANIMATION_COMPLETE";
- case MULTI_WINDOW_CHANGED: return "MULTI_WINDOW_CHANGED";
- case PICTURE_IN_PICTURE_CHANGED: return "PICTURE_IN_PICTURE_CHANGED";
+ case MULTI_WINDOW_MODE_CHANGED: return "MULTI_WINDOW_MODE_CHANGED";
+ case PICTURE_IN_PICTURE_MODE_CHANGED: return "PICTURE_IN_PICTURE_MODE_CHANGED";
case LOCAL_VOICE_INTERACTION_STARTED: return "LOCAL_VOICE_INTERACTION_STARTED";
}
}
@@ -1666,11 +1666,11 @@
case STOP_BINDER_TRACKING_AND_DUMP:
handleStopBinderTrackingAndDump((ParcelFileDescriptor) msg.obj);
break;
- case MULTI_WINDOW_CHANGED:
- handleMultiWindowChanged((IBinder) msg.obj, msg.arg1 == 1);
+ case MULTI_WINDOW_MODE_CHANGED:
+ handleMultiWindowModeChanged((IBinder) msg.obj, msg.arg1 == 1);
break;
- case PICTURE_IN_PICTURE_CHANGED:
- handlePictureInPictureChanged((IBinder) msg.obj, msg.arg1 == 1);
+ case PICTURE_IN_PICTURE_MODE_CHANGED:
+ handlePictureInPictureModeChanged((IBinder) msg.obj, msg.arg1 == 1);
break;
case LOCAL_VOICE_INTERACTION_STARTED:
handleLocalVoiceInteractionStarted((IBinder) ((SomeArgs) msg.obj).arg1,
@@ -2924,17 +2924,17 @@
}
}
- private void handleMultiWindowChanged(IBinder token, boolean inMultiWindow) {
+ private void handleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode) {
final ActivityClientRecord r = mActivities.get(token);
if (r != null) {
- r.activity.onMultiWindowChanged(inMultiWindow);
+ r.activity.onMultiWindowModeChanged(isInMultiWindowMode);
}
}
- private void handlePictureInPictureChanged(IBinder token, boolean inPip) {
+ private void handlePictureInPictureModeChanged(IBinder token, boolean isInPipMode) {
final ActivityClientRecord r = mActivities.get(token);
if (r != null) {
- r.activity.onPictureInPictureChanged(inPip);
+ r.activity.onPictureInPictureModeChanged(isInPipMode);
}
}
@@ -5077,26 +5077,6 @@
*/
TimeZone.setDefault(null);
- synchronized (mResourcesManager) {
- /*
- * Initialize the default locales in this process for the reasons we set the time zone.
- *
- * We do this through ResourcesManager, since we need to do locale negotiation.
- */
- mResourcesManager.setDefaultLocalesLocked(data.config.getLocales());
-
- /*
- * Update the system configuration since its preloaded and might not
- * reflect configuration changes. The configuration object passed
- * in AppBindData can be safely assumed to be up to date
- */
- mResourcesManager.applyConfigurationToResourcesLocked(data.config, data.compatInfo);
- mCurDefaultDisplayDpi = data.config.densityDpi;
-
- // This calls mResourcesManager so keep it within the synchronized block.
- applyCompatConfiguration(mCurDefaultDisplayDpi);
- }
-
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
/**
@@ -5221,6 +5201,26 @@
}
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
+ synchronized (mResourcesManager) {
+ /*
+ * Initialize the default locales in this process for the reasons we set the time zone.
+ *
+ * We do this through ResourcesManager, since we need to do locale negotiation.
+ */
+ mResourcesManager.setDefaultLocalesLocked(data.config.getLocales());
+
+ /*
+ * Update the system configuration since its preloaded and might not
+ * reflect configuration changes. The configuration object passed
+ * in AppBindData can be safely assumed to be up to date
+ */
+ mResourcesManager.applyConfigurationToResourcesLocked(data.config, data.compatInfo);
+ mCurDefaultDisplayDpi = data.config.densityDpi;
+
+ // This calls mResourcesManager so keep it within the synchronized block.
+ applyCompatConfiguration(mCurDefaultDisplayDpi);
+ }
+
if (!Process.isIsolated() && !"android".equals(appContext.getPackageName())) {
// This cache location probably points at credential-encrypted
// storage which may not be accessible yet; assign it anyway instead
diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java
index 744ddf7..ea86dd0 100644
--- a/core/java/android/app/ApplicationThreadNative.java
+++ b/core/java/android/app/ApplicationThreadNative.java
@@ -736,7 +736,7 @@
data.enforceInterface(IApplicationThread.descriptor);
final IBinder b = data.readStrongBinder();
final boolean inMultiWindow = data.readInt() != 0;
- scheduleMultiWindowChanged(b, inMultiWindow);
+ scheduleMultiWindowModeChanged(b, inMultiWindow);
return true;
}
@@ -745,7 +745,7 @@
data.enforceInterface(IApplicationThread.descriptor);
final IBinder b = data.readStrongBinder();
final boolean inPip = data.readInt() != 0;
- schedulePictureInPictureChanged(b, inPip);
+ schedulePictureInPictureModeChanged(b, inPip);
return true;
}
@@ -1498,24 +1498,24 @@
}
@Override
- public final void scheduleMultiWindowChanged(
- IBinder token, boolean inMultiWindow) throws RemoteException {
+ public final void scheduleMultiWindowModeChanged(
+ IBinder token, boolean isInMultiWindowMode) throws RemoteException {
Parcel data = Parcel.obtain();
data.writeInterfaceToken(IApplicationThread.descriptor);
data.writeStrongBinder(token);
- data.writeInt(inMultiWindow ? 1 : 0);
+ data.writeInt(isInMultiWindowMode ? 1 : 0);
mRemote.transact(SCHEDULE_MULTI_WINDOW_CHANGED_TRANSACTION, data, null,
IBinder.FLAG_ONEWAY);
data.recycle();
}
@Override
- public final void schedulePictureInPictureChanged(IBinder token, boolean inPip)
+ public final void schedulePictureInPictureModeChanged(IBinder token, boolean isInPipMode)
throws RemoteException {
Parcel data = Parcel.obtain();
data.writeInterfaceToken(IApplicationThread.descriptor);
data.writeStrongBinder(token);
- data.writeInt(inPip ? 1 : 0);
+ data.writeInt(isInPipMode ? 1 : 0);
mRemote.transact(SCHEDULE_PICTURE_IN_PICTURE_CHANGED_TRANSACTION, data, null,
IBinder.FLAG_ONEWAY);
data.recycle();
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index d28f1fb..6bb853a 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1993,7 +1993,7 @@
ContextImpl context = new ContextImpl(null, mainThread,
packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
- context.mResourcesManager.getDisplayMetricsLocked());
+ context.mResourcesManager.getDisplayMetrics());
return context;
}
@@ -2065,16 +2065,34 @@
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
- resources = mResourcesManager.getResources(
- activityToken,
- packageInfo.getResDir(),
- packageInfo.getSplitResDirs(),
- packageInfo.getOverlayDirs(),
- packageInfo.getApplicationInfo().sharedLibraryFiles,
- displayId,
- overrideConfiguration,
- compatInfo,
- packageInfo.getClassLoader());
+
+ if (container != null) {
+ // This is a nested Context, so it can't be a base Activity context.
+ // Just create a regular Resources object associated with the Activity.
+ resources = mResourcesManager.getResources(
+ activityToken,
+ packageInfo.getResDir(),
+ packageInfo.getSplitResDirs(),
+ packageInfo.getOverlayDirs(),
+ packageInfo.getApplicationInfo().sharedLibraryFiles,
+ displayId,
+ overrideConfiguration,
+ compatInfo,
+ packageInfo.getClassLoader());
+ } else {
+ // This is not a nested Context, so it must be the root Activity context.
+ // All other nested Contexts will inherit the configuration set here.
+ resources = mResourcesManager.createBaseActivityResources(
+ activityToken,
+ packageInfo.getResDir(),
+ packageInfo.getSplitResDirs(),
+ packageInfo.getOverlayDirs(),
+ packageInfo.getApplicationInfo().sharedLibraryFiles,
+ displayId,
+ overrideConfiguration,
+ compatInfo,
+ packageInfo.getClassLoader());
+ }
}
}
mResources = resources;
diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java
index f7a4557..bb3c719 100644
--- a/core/java/android/app/Fragment.java
+++ b/core/java/android/app/Fragment.java
@@ -1582,21 +1582,21 @@
/**
* Called when the Fragment's activity changes from fullscreen mode to multi-window mode and
- * visa-versa. This is generally tied to {@link Activity#onMultiWindowChanged} of the containing
- * Activity.
+ * visa-versa. This is generally tied to {@link Activity#onMultiWindowModeChanged} of the
+ * containing Activity.
*
- * @param inMultiWindow True if the activity is in multi-window mode.
+ * @param isInMultiWindowMode True if the activity is in multi-window mode.
*/
- public void onMultiWindowChanged(boolean inMultiWindow) {
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
}
/**
* Called by the system when the activity changes to and from picture-in-picture mode. This is
- * generally tied to {@link Activity#onPictureInPictureChanged} of the containing Activity.
+ * generally tied to {@link Activity#onPictureInPictureModeChanged} of the containing Activity.
*
- * @param inPictureInPicture True if the activity is in picture-in-picture mode.
+ * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode.
*/
- public void onPictureInPictureChanged(boolean inPictureInPicture) {
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
}
public void onConfigurationChanged(Configuration newConfig) {
@@ -2334,17 +2334,17 @@
}
}
- void performMultiWindowChanged(boolean inMultiWindow) {
- onMultiWindowChanged(inMultiWindow);
+ void performMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ onMultiWindowModeChanged(isInMultiWindowMode);
if (mChildFragmentManager != null) {
- mChildFragmentManager.dispatchMultiWindowChanged(inMultiWindow);
+ mChildFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode);
}
}
- void performPictureInPictureChanged(boolean inPictureInPicture) {
- onPictureInPictureChanged(inPictureInPicture);
+ void performPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ onPictureInPictureModeChanged(isInPictureInPictureMode);
if (mChildFragmentManager != null) {
- mChildFragmentManager.dispatchPictureInPictureChanged(inPictureInPicture);
+ mChildFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode);
}
}
diff --git a/core/java/android/app/FragmentController.java b/core/java/android/app/FragmentController.java
index 57b0ff1..b3d2df5 100644
--- a/core/java/android/app/FragmentController.java
+++ b/core/java/android/app/FragmentController.java
@@ -247,10 +247,10 @@
* the activity changed.
* <p>Call when the multi-window mode of the activity changed.
*
- * @see Fragment#onMultiWindowChanged
+ * @see Fragment#onMultiWindowModeChanged
*/
- public void dispatchMultiWindowChanged(boolean inMultiWindow) {
- mHost.mFragmentManager.dispatchMultiWindowChanged(inMultiWindow);
+ public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ mHost.mFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode);
}
/**
@@ -258,10 +258,10 @@
* mode of the activity changed.
* <p>Call when the picture-in-picture mode of the activity changed.
*
- * @see Fragment#onPictureInPictureChanged
+ * @see Fragment#onPictureInPictureModeChanged
*/
- public void dispatchPictureInPictureChanged(boolean inPictureInPicture) {
- mHost.mFragmentManager.dispatchPictureInPictureChanged(inPictureInPicture);
+ public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
+ mHost.mFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode);
}
/**
diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java
index 2852baf..8369f17 100644
--- a/core/java/android/app/FragmentManager.java
+++ b/core/java/android/app/FragmentManager.java
@@ -2069,26 +2069,26 @@
mParent = null;
}
- public void dispatchMultiWindowChanged(boolean inMultiWindow) {
+ public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode) {
if (mAdded == null) {
return;
}
for (int i = mAdded.size() - 1; i >= 0; --i) {
final Fragment f = mAdded.get(i);
if (f != null) {
- f.performMultiWindowChanged(inMultiWindow);
+ f.performMultiWindowModeChanged(isInMultiWindowMode);
}
}
}
- public void dispatchPictureInPictureChanged(boolean inPictureInPicture) {
+ public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
if (mAdded == null) {
return;
}
for (int i = mAdded.size() - 1; i >= 0; --i) {
final Fragment f = mAdded.get(i);
if (f != null) {
- f.performPictureInPictureChanged(inPictureInPicture);
+ f.performPictureInPictureModeChanged(isInPictureInPictureMode);
}
}
}
diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java
index 417c0679..8ee6fd0 100644
--- a/core/java/android/app/IActivityManager.java
+++ b/core/java/android/app/IActivityManager.java
@@ -623,11 +623,11 @@
public int getAppStartMode(int uid, String packageName) throws RemoteException;
- public boolean inMultiWindow(IBinder token) throws RemoteException;
+ public boolean isInMultiWindowMode(IBinder token) throws RemoteException;
- public boolean inPictureInPicture(IBinder token) throws RemoteException;
+ public boolean isInPictureInPictureMode(IBinder token) throws RemoteException;
- public void enterPictureInPicture(IBinder token) throws RemoteException;
+ public void enterPictureInPictureMode(IBinder token) throws RemoteException;
public int setVrMode(IBinder token, boolean enabled, ComponentName packageName)
throws RemoteException;
diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java
index 628bde0..a3b2638 100644
--- a/core/java/android/app/IApplicationThread.java
+++ b/core/java/android/app/IApplicationThread.java
@@ -158,8 +158,8 @@
void notifyCleartextNetwork(byte[] firstPacket) throws RemoteException;
void startBinderTracking() throws RemoteException;
void stopBinderTrackingAndDump(FileDescriptor fd) throws RemoteException;
- void scheduleMultiWindowChanged(IBinder token, boolean multiWindowMode) throws RemoteException;
- void schedulePictureInPictureChanged(IBinder token, boolean multiWindowMode) throws RemoteException;
+ void scheduleMultiWindowModeChanged(IBinder token, boolean isInMultiWindowMode) throws RemoteException;
+ void schedulePictureInPictureModeChanged(IBinder token, boolean isInPictureInPictureMode) throws RemoteException;
void scheduleLocalVoiceInteractionStarted(IBinder token, IVoiceInteractor voiceInteractor) throws RemoteException;
String descriptor = "android.app.IApplicationThread";
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 54d813d..3f22385 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -66,7 +66,7 @@
}
};
- private String[] mSystemLocales = {};
+ private String[] mSystemLocales = null;
private final HashSet<String> mNonSystemLocales = new HashSet<>();
private boolean mHasNonSystemLocales = false;
@@ -94,9 +94,18 @@
private final ArrayList<WeakReference<Resources>> mResourceReferences = new ArrayList<>();
/**
- * Each Activity may have only one Resources object.
+ * Resources and base configuration override associated with an Activity.
*/
- private final WeakHashMap<IBinder, WeakReference<Resources>> mActivityResourceReferences =
+ private static class ActivityResources {
+ public final Configuration overrideConfig = new Configuration();
+ public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>();
+ }
+
+ /**
+ * Each Activity may has a base override configuration that is applied to each Resources object,
+ * which in turn may have their own override configuration specified.
+ */
+ private final WeakHashMap<IBinder, ActivityResources> mActivityResourceReferences =
new WeakHashMap<>();
/**
@@ -115,18 +124,20 @@
}
public Configuration getConfiguration() {
- return mResConfiguration;
+ synchronized (this) {
+ return mResConfiguration;
+ }
}
- DisplayMetrics getDisplayMetricsLocked() {
- return getDisplayMetricsLocked(Display.DEFAULT_DISPLAY);
+ DisplayMetrics getDisplayMetrics() {
+ return getDisplayMetrics(Display.DEFAULT_DISPLAY);
}
/**
* Protected so that tests can override and returns something a fixed value.
*/
@VisibleForTesting
- protected DisplayMetrics getDisplayMetricsLocked(int displayId) {
+ protected DisplayMetrics getDisplayMetrics(int displayId) {
DisplayMetrics dm = new DisplayMetrics();
final Display display =
getAdjustedDisplay(displayId, DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS);
@@ -272,10 +283,9 @@
return config;
}
-
private ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
AssetManager assets = createAssetManager(key);
- DisplayMetrics dm = getDisplayMetricsLocked(key.mDisplayId);
+ DisplayMetrics dm = getDisplayMetrics(key.mDisplayId);
Configuration config = generateConfig(key, dm);
ResourcesImpl impl = new ResourcesImpl(assets, dm, config, key.mCompatInfo);
if (DEBUG) {
@@ -290,7 +300,7 @@
* @param key The key to match.
* @return a ResourcesImpl if the key matches a cache entry, null otherwise.
*/
- private ResourcesImpl findResourcesImplForKey(@NonNull ResourcesKey key) {
+ private ResourcesImpl findResourcesImplForKeyLocked(@NonNull ResourcesKey key) {
WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.get(key);
ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
if (impl != null && impl.getAssets().isUpToDate()) {
@@ -303,7 +313,7 @@
* Find the ResourcesKey that this ResourcesImpl object is associated with.
* @return the ResourcesKey or null if none was found.
*/
- private ResourcesKey findKeyForResourceImpl(@NonNull ResourcesImpl resourceImpl) {
+ private ResourcesKey findKeyForResourceImplLocked(@NonNull ResourcesImpl resourceImpl) {
final int refCount = mResourceImpls.size();
for (int i = 0; i < refCount; i++) {
WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.valueAt(i);
@@ -315,36 +325,46 @@
return null;
}
+ private ActivityResources getOrCreateActivityResourcesStructLocked(
+ @NonNull IBinder activityToken) {
+ ActivityResources activityResources = mActivityResourceReferences.get(activityToken);
+ if (activityResources == null) {
+ activityResources = new ActivityResources();
+ mActivityResourceReferences.put(activityToken, activityResources);
+ }
+ return activityResources;
+ }
+
/**
* Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist
* or the class loader is different.
*/
private Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken,
@NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl) {
- // This is a request tied to an Activity, meaning we will need to update all
- // Activity related Resources to match this configuration.
- WeakReference<Resources> weakResourceRef = mActivityResourceReferences.get(activityToken);
- Resources resources = weakResourceRef != null ? weakResourceRef.get() : null;
- if (resources == null || !Objects.equals(resources.getClassLoader(), classLoader)) {
- resources = new Resources(classLoader);
- mActivityResourceReferences.put(activityToken, new WeakReference<>(resources));
- if (DEBUG) {
- Slog.d(TAG, "- creating new ref=" + resources);
- }
- } else {
- if (DEBUG) {
- Slog.d(TAG, "- using existing ref=" + resources);
+ final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
+ activityToken);
+
+ final int refCount = activityResources.activityResources.size();
+ for (int i = 0; i < refCount; i++) {
+ WeakReference<Resources> weakResourceRef = activityResources.activityResources.get(i);
+ Resources resources = weakResourceRef.get();
+
+ if (resources != null
+ && Objects.equals(resources.getClassLoader(), classLoader)
+ && resources.getImpl() == impl) {
+ if (DEBUG) {
+ Slog.d(TAG, "- using existing ref=" + resources);
+ }
+ return resources;
}
}
- if (resources.getImpl() != impl) {
- if (DEBUG) {
- Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
- }
-
- // Setting an impl is expensive because we update all ThemeImpl references.
- // too.
- resources.setImpl(impl);
+ Resources resources = new Resources(classLoader);
+ resources.setImpl(impl);
+ activityResources.activityResources.add(new WeakReference<>(resources));
+ if (DEBUG) {
+ Slog.d(TAG, "- creating new ref=" + resources);
+ Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
}
return resources;
}
@@ -359,7 +379,7 @@
final int refCount = mResourceReferences.size();
for (int i = 0; i < refCount; i++) {
WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
- Resources resources = weakResourceRef != null ? weakResourceRef.get() : null;
+ Resources resources = weakResourceRef.get();
if (resources != null &&
Objects.equals(resources.getClassLoader(), classLoader) &&
resources.getImpl() == impl) {
@@ -382,6 +402,177 @@
}
/**
+ * Creates base resources for an Activity. Calls to
+ * {@link #getResources(IBinder, String, String[], String[], String[], int, Configuration,
+ * CompatibilityInfo, ClassLoader)} with the same activityToken will have their override
+ * configurations merged with the one specified here.
+ *
+ * @param activityToken Represents an Activity.
+ * @param resDir The base resource path. Can be null (only framework resources will be loaded).
+ * @param splitResDirs An array of split resource paths. Can be null.
+ * @param overlayDirs An array of overlay paths. Can be null.
+ * @param libDirs An array of resource library paths. Can be null.
+ * @param displayId The ID of the display for which to create the resources.
+ * @param overrideConfig The configuration to apply on top of the base configuration. Can be
+ * null. This provides the base override for this Activity.
+ * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is
+ * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}.
+ * @param classLoader The class loader to use when inflating Resources. If null, the
+ * {@link ClassLoader#getSystemClassLoader()} is used.
+ * @return a Resources object from which to access resources.
+ */
+ public Resources createBaseActivityResources(@NonNull IBinder activityToken,
+ @Nullable String resDir,
+ @Nullable String[] splitResDirs,
+ @Nullable String[] overlayDirs,
+ @Nullable String[] libDirs,
+ int displayId,
+ @Nullable Configuration overrideConfig,
+ @NonNull CompatibilityInfo compatInfo,
+ @Nullable ClassLoader classLoader) {
+ final ResourcesKey key = new ResourcesKey(
+ resDir,
+ splitResDirs,
+ overlayDirs,
+ libDirs,
+ displayId,
+ overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
+ compatInfo);
+ classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
+
+ synchronized (this) {
+ final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
+ activityToken);
+
+ if (overrideConfig != null) {
+ activityResources.overrideConfig.setTo(overrideConfig);
+ } else {
+ activityResources.overrideConfig.setToDefaults();
+ }
+ }
+
+ // Update any existing Activity Resources references.
+ updateResourcesForActivity(activityToken, overrideConfig);
+
+ // Now request an actual Resources object.
+ return getOrCreateResources(activityToken, key, classLoader);
+ }
+
+ /**
+ * Gets an existing Resources object set with a ResourcesImpl object matching the given key,
+ * or creates one if it doesn't exist.
+ *
+ * @param activityToken The Activity this Resources object should be associated with.
+ * @param key The key describing the parameters of the ResourcesImpl object.
+ * @param classLoader The classloader to use for the Resources object.
+ * If null, {@link ClassLoader#getSystemClassLoader()} is used.
+ * @return A Resources object that gets updated when
+ * {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)}
+ * is called.
+ */
+ private Resources getOrCreateResources(@Nullable IBinder activityToken,
+ @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
+ final boolean findSystemLocales;
+ final boolean hasNonSystemLocales;
+ synchronized (this) {
+ findSystemLocales = (mSystemLocales == null || mSystemLocales.length == 0);
+ hasNonSystemLocales = mHasNonSystemLocales;
+
+ if (DEBUG) {
+ Throwable here = new Throwable();
+ here.fillInStackTrace();
+ Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
+ }
+
+ if (activityToken != null) {
+ final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
+ activityToken);
+
+ // Clean up any dead references so they don't pile up.
+ ArrayUtils.unstableRemoveIf(activityResources.activityResources,
+ sEmptyReferencePredicate);
+
+ // Rebase the key's override config on top of the Activity's base override.
+ if (key.hasOverrideConfiguration()
+ && !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
+ final Configuration temp = new Configuration(activityResources.overrideConfig);
+ temp.updateFrom(key.mOverrideConfiguration);
+ key.mOverrideConfiguration.setTo(temp);
+ }
+
+ ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
+ if (resourcesImpl != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "- using existing impl=" + resourcesImpl);
+ }
+ return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
+ resourcesImpl);
+ }
+
+ // We will create the ResourcesImpl object outside of holding this lock.
+
+ } else {
+ // Clean up any dead references so they don't pile up.
+ ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
+
+ // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
+ ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
+ if (resourcesImpl != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "- using existing impl=" + resourcesImpl);
+ }
+ return getOrCreateResourcesLocked(classLoader, resourcesImpl);
+ }
+
+ // We will create the ResourcesImpl object outside of holding this lock.
+ }
+ }
+
+ // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
+ ResourcesImpl resourcesImpl = createResourcesImpl(key);
+
+ final String[] systemLocales = findSystemLocales
+ ? AssetManager.getSystem().getLocales() : null;
+ final String[] nonSystemLocales = resourcesImpl.getAssets().getNonSystemLocales();
+ // Avoid checking for non-pseudo-locales if we already know there were some from a previous
+ // Resources. The default value (for when hasNonSystemLocales is true) doesn't matter,
+ // since mHasNonSystemLocales will also be true, and thus isPseudoLocalesOnly would not be
+ // able to affect mHasNonSystemLocales.
+ final boolean isPseudoLocalesOnly = hasNonSystemLocales ||
+ LocaleList.isPseudoLocalesOnly(nonSystemLocales);
+
+ synchronized (this) {
+ if (mSystemLocales == null || mSystemLocales.length == 0) {
+ mSystemLocales = systemLocales;
+ }
+ mNonSystemLocales.addAll(Arrays.asList(nonSystemLocales));
+ mHasNonSystemLocales = mHasNonSystemLocales || !isPseudoLocalesOnly;
+
+ ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
+ if (existingResourcesImpl != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "- got beat! existing impl=" + existingResourcesImpl
+ + " new impl=" + resourcesImpl);
+ }
+ resourcesImpl.getAssets().close();
+ resourcesImpl = existingResourcesImpl;
+ } else {
+ // Add this ResourcesImpl to the cache.
+ mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
+ }
+
+ final Resources resources;
+ if (activityToken != null) {
+ resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
+ resourcesImpl);
+ } else {
+ resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
+ }
+ return resources;
+ }
+ }
+
+ /**
* Gets or creates a new Resources object associated with the IBinder token. References returned
* by this method live as long as the Activity, meaning they can be cached and used by the
* Activity even after a configuration change. If any other parameter is changed
@@ -425,92 +616,8 @@
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
-
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
-
- final boolean findSystemLocales;
- final boolean hasNonSystemLocales;
- synchronized (this) {
- findSystemLocales = (mSystemLocales.length == 0);
- hasNonSystemLocales = mHasNonSystemLocales;
-
- if (DEBUG) {
- Throwable here = new Throwable();
- here.fillInStackTrace();
- Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
- }
-
- if (activityToken != null) {
- ResourcesImpl resourcesImpl = findResourcesImplForKey(key);
- if (resourcesImpl != null) {
- if (DEBUG) {
- Slog.d(TAG, "- using existing impl=" + resourcesImpl);
- }
- return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
- resourcesImpl);
- }
-
- // We will create the ResourcesImpl object outside of holding this lock.
-
- } else {
- // Clean up any dead references so they don't pile up.
- ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
-
- // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
- ResourcesImpl resourcesImpl = findResourcesImplForKey(key);
- if (resourcesImpl != null) {
- if (DEBUG) {
- Slog.d(TAG, "- using existing impl=" + resourcesImpl);
- }
- return getOrCreateResourcesLocked(classLoader, resourcesImpl);
- }
-
- // We will create the ResourcesImpl object outside of holding this lock.
- }
- }
-
- // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
- ResourcesImpl resourcesImpl = createResourcesImpl(key);
-
- final String[] systemLocales = findSystemLocales
- ? AssetManager.getSystem().getLocales() : null;
- final String[] nonSystemLocales = resourcesImpl.getAssets().getNonSystemLocales();
- // Avoid checking for non-pseudo-locales if we already know there were some from a previous
- // Resources. The default value (for when hasNonSystemLocales is true) doesn't matter,
- // since mHasNonSystemLocales will also be true, and thus isPseudoLocalesOnly would not be
- // able to affect mHasNonSystemLocales.
- final boolean isPseudoLocalesOnly = hasNonSystemLocales ||
- LocaleList.isPseudoLocalesOnly(nonSystemLocales);
-
- synchronized (this) {
- if (mSystemLocales.length == 0) {
- mSystemLocales = systemLocales;
- }
- mNonSystemLocales.addAll(Arrays.asList(nonSystemLocales));
- mHasNonSystemLocales = mHasNonSystemLocales || !isPseudoLocalesOnly;
-
- ResourcesImpl existingResourcesImpl = findResourcesImplForKey(key);
- if (existingResourcesImpl != null) {
- if (DEBUG) {
- Slog.d(TAG, "- got beat! existing impl=" + existingResourcesImpl
- + " new impl=" + resourcesImpl);
- }
- resourcesImpl.getAssets().close();
- resourcesImpl = existingResourcesImpl;
- } else {
- // Add this ResourcesImpl to the cache.
- mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
- }
-
- final Resources resources;
- if (activityToken != null) {
- resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
- resourcesImpl);
- } else {
- resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
- }
- return resources;
- }
+ return getOrCreateResources(activityToken, key, classLoader);
}
/**
@@ -524,34 +631,84 @@
*/
public void updateResourcesForActivity(@NonNull IBinder activityToken,
@Nullable Configuration overrideConfig) {
- final ClassLoader classLoader;
- final ResourcesKey oldKey;
synchronized (this) {
- // Extract the ResourcesKey that was last used to create the Resources for this
- // activity.
- WeakReference<Resources> weakResRef = mActivityResourceReferences.get(activityToken);
- final Resources resources = weakResRef != null ? weakResRef.get() : null;
- if (resources == null) {
- Slog.e(TAG, "can't update resources for uncached activity " + activityToken);
+ final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
+ activityToken);
+
+ if (Objects.equals(activityResources.overrideConfig, overrideConfig)) {
+ // They are the same, no work to do.
return;
}
- classLoader = resources.getClassLoader();
- oldKey = findKeyForResourceImpl(resources.getImpl());
- if (oldKey == null) {
- Slog.e(TAG, "can't find ResourcesKey for resources impl=" + resources.getImpl());
- return;
+ // Grab a copy of the old configuration so we can create the delta's of each
+ // Resources object associated with this Activity.
+ final Configuration oldConfig = new Configuration(activityResources.overrideConfig);
+
+ // Update the Activity's base override.
+ if (overrideConfig != null) {
+ activityResources.overrideConfig.setTo(overrideConfig);
+ } else {
+ activityResources.overrideConfig.setToDefaults();
+ }
+
+ final boolean activityHasOverrideConfig =
+ !activityResources.overrideConfig.equals(Configuration.EMPTY);
+
+ // Rebase each Resources associated with this Activity.
+ final int refCount = activityResources.activityResources.size();
+ for (int i = 0; i < refCount; i++) {
+ WeakReference<Resources> weakResRef = activityResources.activityResources.get(i);
+ Resources resources = weakResRef.get();
+ if (resources == null) {
+ continue;
+ }
+
+ // Extract the ResourcesKey that was last used to create the Resources for this
+ // activity.
+ final ResourcesKey oldKey = findKeyForResourceImplLocked(resources.getImpl());
+ if (oldKey == null) {
+ Slog.e(TAG, "can't find ResourcesKey for resources impl="
+ + resources.getImpl());
+ continue;
+ }
+
+ // Build the new override configuration for this ResourcesKey.
+ final Configuration rebasedOverrideConfig = new Configuration();
+ if (overrideConfig != null) {
+ rebasedOverrideConfig.setTo(overrideConfig);
+ }
+
+ if (activityHasOverrideConfig && oldKey.hasOverrideConfiguration()) {
+ // Generate a delta between the old base Activity override configuration and
+ // the actual final override configuration that was used to figure out the real
+ // delta this Resources object wanted.
+ Configuration overrideOverrideConfig = Configuration.generateDelta(
+ oldConfig, oldKey.mOverrideConfiguration);
+ rebasedOverrideConfig.updateFrom(overrideOverrideConfig);
+ }
+
+ // Create the new ResourcesKey with the rebased override config.
+ final ResourcesKey newKey = new ResourcesKey(oldKey.mResDir, oldKey.mSplitResDirs,
+ oldKey.mOverlayDirs, oldKey.mLibDirs, oldKey.mDisplayId,
+ rebasedOverrideConfig, oldKey.mCompatInfo);
+
+ ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(newKey);
+ if (resourcesImpl == null) {
+ resourcesImpl = createResourcesImpl(newKey);
+ }
+
+ if (resourcesImpl != resources.getImpl()) {
+ // Set the ResourcesImpl, updating it for all users of this Resources object.
+ resources.setImpl(resourcesImpl);
+ }
}
}
-
- // Update the Resources object with the new override config and all of the existing
- // settings.
- getResources(activityToken, oldKey.mResDir, oldKey.mSplitResDirs, oldKey.mOverlayDirs,
- oldKey.mLibDirs, oldKey.mDisplayId, overrideConfig, oldKey.mCompatInfo,
- classLoader);
}
/* package */ void setDefaultLocalesLocked(@NonNull LocaleList locales) {
+ if (mSystemLocales == null) {
+ throw new RuntimeException("ResourcesManager is not ready to negotiate locales.");
+ }
final int bestLocale;
if (mHasNonSystemLocales) {
bestLocale = locales.getFirstMatchIndexWithEnglishSupported(mNonSystemLocales);
@@ -575,7 +732,7 @@
int changes = mResConfiguration.updateFrom(config);
// Things might have changed in display manager, so clear the cached displays.
mDisplays.clear();
- DisplayMetrics defaultDisplayMetrics = getDisplayMetricsLocked();
+ DisplayMetrics defaultDisplayMetrics = getDisplayMetrics();
if (compat != null && (mResCompatibilityInfo == null ||
!mResCompatibilityInfo.equals(compat))) {
@@ -629,7 +786,7 @@
}
tmpConfig.setTo(localeAdjustedConfig);
if (!isDefaultDisplay) {
- dm = getDisplayMetricsLocked(displayId);
+ dm = getDisplayMetrics(displayId);
applyNonDefaultDisplayMetricsToConfiguration(dm, tmpConfig);
}
if (hasOverrideConfiguration) {
@@ -649,4 +806,4 @@
return changes != 0;
}
-}
\ No newline at end of file
+}
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 2987fbc..bdc4404 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -882,7 +882,12 @@
public final T getService(ContextImpl ctx) {
synchronized (StaticApplicationContextServiceFetcher.this) {
if (mCachedInstance == null) {
- mCachedInstance = createService(ctx.getApplicationContext());
+ Context appContext = ctx.getApplicationContext();
+ // If the application context is null, we're either in the system process or
+ // it's the application context very early in app initialization. In both these
+ // cases, the passed-in ContextImpl will not be freed, so it's safe to pass it
+ // to the service. http://b/27532714 .
+ mCachedInstance = createService(appContext != null ? appContext : ctx);
}
return mCachedInstance;
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 36c82e5..6b4771c 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -4889,15 +4889,30 @@
* @throws SecurityException if {@code admin} is not a device or profile owner.
*/
public Bundle getUserRestrictions(@NonNull ComponentName admin) {
- return getUserRestrictions(admin, myUserId());
- }
-
- /** @hide per-user version */
- public Bundle getUserRestrictions(@NonNull ComponentName admin, int userHandle) {
Bundle ret = null;
if (mService != null) {
try {
- ret = mService.getUserRestrictions(admin, userHandle);
+ ret = mService.getUserRestrictions(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return ret == null ? new Bundle() : ret;
+ }
+
+ /**
+ * Called by the system to get the user restrictions for a user.
+ *
+ * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param userHandle user id the admin is running as.
+ *
+ * @hide
+ */
+ public Bundle getUserRestrictionsForUser(@NonNull ComponentName admin, int userHandle) {
+ Bundle ret = null;
+ if (mService != null) {
+ try {
+ ret = mService.getUserRestrictionsForUser(admin, userHandle);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 8be52d8..c3d5ed9 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -169,7 +169,8 @@
ComponentName getRestrictionsProvider(int userHandle);
void setUserRestriction(in ComponentName who, in String key, boolean enable);
- Bundle getUserRestrictions(in ComponentName who, int userId);
+ Bundle getUserRestrictions(in ComponentName who);
+ Bundle getUserRestrictionsForUser(in ComponentName who, int userId);
void addCrossProfileIntentFilter(in ComponentName admin, in IntentFilter filter, int flags);
void clearCrossProfileIntentFilters(in ComponentName admin);
diff --git a/core/java/android/bluetooth/BluetoothPbapClient.java b/core/java/android/bluetooth/BluetoothPbapClient.java
index 736e55d..eab4c6f 100644
--- a/core/java/android/bluetooth/BluetoothPbapClient.java
+++ b/core/java/android/bluetooth/BluetoothPbapClient.java
@@ -40,7 +40,6 @@
"android.bluetooth.pbap.profile.action.CONNECTION_STATE_CHANGED";
private IBluetoothPbapClient mService;
- private BluetoothDevice mDevice;
private final Context mContext;
private ServiceListener mServiceListener;
private BluetoothAdapter mAdapter;
@@ -173,7 +172,6 @@
}
if (mService != null && isEnabled() && isValidDevice(device)) {
try {
- mDevice = device;
return mService.connect(device);
} catch (RemoteException e) {
Log.e(TAG, Log.getStackTraceString(new Throwable()));
@@ -193,13 +191,13 @@
* @return false on error,
* true otherwise
*/
- public boolean disconnect() {
+ public boolean disconnect(BluetoothDevice device) {
if (DBG) {
- log("disconnect(" + mDevice + ")");
+ log("disconnect(" + device + ")" + new Exception() );
}
- if (mService != null && isEnabled() && isValidDevice(mDevice)) {
+ if (mService != null && isEnabled() && isValidDevice(device)) {
try {
- mService.disconnect(mDevice);
+ mService.disconnect(device);
return true;
} catch (RemoteException e) {
Log.e(TAG, Log.getStackTraceString(new Throwable()));
@@ -328,4 +326,66 @@
}
return false;
}
+
+ /**
+ * Set priority of the profile
+ *
+ * <p> The device should already be paired.
+ * Priority can be one of {@link #PRIORITY_ON} or
+ * {@link #PRIORITY_OFF},
+ *
+ * @param device Paired bluetooth device
+ * @param priority
+ * @return true if priority is set, false on error
+ */
+ public boolean setPriority(BluetoothDevice device, int priority) {
+ if (DBG) {
+ log("setPriority(" + device + ", " + priority + ")");
+ }
+ if (mService != null && isEnabled() &&
+ isValidDevice(device)) {
+ if (priority != BluetoothProfile.PRIORITY_OFF &&
+ priority != BluetoothProfile.PRIORITY_ON) {
+ return false;
+ }
+ try {
+ return mService.setPriority(device, priority);
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ return false;
+ }
+ }
+ if (mService == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ return false;
+ }
+
+ /**
+ * Get the priority of the profile.
+ *
+ * <p> The priority can be any of:
+ * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF},
+ * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED}
+ *
+ * @param device Bluetooth device
+ * @return priority of the device
+ */
+ public int getPriority(BluetoothDevice device) {
+ if (VDBG) {
+ log("getPriority(" + device + ")");
+ }
+ if (mService != null && isEnabled() && isValidDevice(device)) {
+ try {
+ return mService.getPriority(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ return PRIORITY_OFF;
+ }
+ }
+ if (mService == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ return PRIORITY_OFF;
+ }
}
diff --git a/core/java/android/bluetooth/IBluetoothPbapClient.aidl b/core/java/android/bluetooth/IBluetoothPbapClient.aidl
index b26ea29..6d4c5a6 100644
--- a/core/java/android/bluetooth/IBluetoothPbapClient.aidl
+++ b/core/java/android/bluetooth/IBluetoothPbapClient.aidl
@@ -29,4 +29,6 @@
List<BluetoothDevice> getConnectedDevices();
List<BluetoothDevice> getDevicesMatchingConnectionStates(in int[] states);
int getConnectionState(in BluetoothDevice device);
+ boolean setPriority(in BluetoothDevice device, int priority);
+ int getPriority(in BluetoothDevice device);
}
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index 7408c34..fbe35a65 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -34,18 +34,20 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+// TODO Enhance javadoc
/**
- * TODO Enhance javadoc
*
- * Represents a shortcut form an application.
+ * Represents a shortcut from an application.
*
- * Notes...
- * - If an {@link Icon} is of a resource, then we'll just persist the package name and resource ID.
+ * <p>Notes about icons:
+ * <ul>
+ * <li>If an {@link Icon} is a resource, the system keeps the package name and the resource ID.
+ * Otherwise, the bitmap is fetched when it's registered to ShortcutManager,
+ * then shrunk if necessary, and persisted.
+ * <li>The system disallows byte[] icons, because they can easily go over the binder size limit.
+ * </ul>
*
- * Otherwise, the bitmap will be fetched when it's registered to ShortcutManager, then *shrunk*
- * if necessary, and persisted.
- *
- * We will disallow byte[] icons, because they can easily go over binder size limit.
+ * @see {@link ShortcutManager}.
*/
public class ShortcutInfo implements Parcelable {
/* @hide */
@@ -371,6 +373,9 @@
/**
* Sets the title of a shortcut. This is a mandatory field.
+ *
+ * <p>This field is intended for a concise description of a shortcut displayed under
+ * an icon. The recommend max length is 10 characters.
*/
@NonNull
public Builder setTitle(@NonNull String title) {
@@ -380,6 +385,9 @@
/**
* Sets the text of a shortcut. This is an optional field.
+ *
+ * <p>This field is intended to be more descriptive than the shortcut title.
+ * The recommend max length is 25 characters.
*/
@NonNull
public Builder setText(@NonNull String text) {
diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java
index b247f65..e4a98b5 100644
--- a/core/java/android/content/pm/ShortcutManager.java
+++ b/core/java/android/content/pm/ShortcutManager.java
@@ -19,15 +19,13 @@
import android.content.Context;
import android.os.RemoteException;
import android.os.UserHandle;
-import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.util.List;
+// TODO Enhance javadoc
/**
- * TODO Enhance javadoc
- *
* {@link ShortcutManager} manages shortcuts created by applications.
*
* <h3>Dynamic shortcuts and pinned shortcuts</h3>
@@ -66,15 +64,18 @@
* {@link #getRemainingCallCount()} times until the rate-limiting counter is reset,
* which happens at a certain time every day.
*
- * <p>An applications can use {@link #getRateLimitResetTime()} to get the next reset time.
+ * <p>An application can use {@link #getRateLimitResetTime()} to get the next reset time.
+ *
+ * <p>For testing purposes, use "Developer Options" (found in the Settings menu) to reset the
+ * internal rate-limiting counter. Automated tests can use the following ADB shell command to
+ * achieve the same effect:</p>
+ * <pre>adb shell cmd shortcut reset-throttling</pre>
*
* <h3>Backup and Restore</h3>
*
* Shortcuts will be backed up and restored across devices. This means all information, including
* IDs, must be meaningful on a different device.
*
- * TODO: Define a Broadcast to let apps update shortcuts on a restored device.
- *
* <h3>APIs for launcher</h3>
*
* Launcher applications should use {@link LauncherApps} to get shortcuts that are published from
diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java
index b75e653..7d903bd 100644
--- a/core/java/android/hardware/SystemSensorManager.java
+++ b/core/java/android/hardware/SystemSensorManager.java
@@ -31,12 +31,15 @@
import android.util.SparseIntArray;
import dalvik.system.CloseGuard;
+import com.android.internal.annotations.GuardedBy;
+
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+
/**
* Sensor manager implementation that communicates with the built-in
* system sensors.
@@ -55,7 +58,9 @@
private static native boolean nativeIsDataInjectionEnabled(long nativeInstance);
private static final Object sLock = new Object();
- private static boolean sSensorModuleInitialized = false;
+ @GuardedBy("sLock")
+ private static boolean sNativeClassInited = false;
+ @GuardedBy("sLock")
private static InjectEventQueue sInjectEventQueue = null;
private final ArrayList<Sensor> mFullSensorsList = new ArrayList<>();
@@ -84,8 +89,8 @@
/** {@hide} */
public SystemSensorManager(Context context, Looper mainLooper) {
synchronized(sLock) {
- if (!sSensorModuleInitialized) {
- sSensorModuleInitialized = true;
+ if (!sNativeClassInited) {
+ sNativeClassInited = true;
nativeClassInit();
}
}
diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java
index 8738424..e464a4a 100644
--- a/core/java/android/net/NetworkPolicyManager.java
+++ b/core/java/android/net/NetworkPolicyManager.java
@@ -57,6 +57,10 @@
public static final int RULE_REJECT_METERED = 1;
/** Reject traffic on all networks. */
public static final int RULE_REJECT_ALL = 2;
+ /** Allow traffic on metered networks. */
+ public static final int RULE_ALLOW_METERED = 3;
+ /** Temporarily allow traffic on metered networks because app is on foreground. */
+ public static final int RULE_TEMPORARY_ALLOW_METERED = 4;
public static final int FIREWALL_RULE_DEFAULT = 0;
public static final int FIREWALL_RULE_ALLOW = 1;
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 8bc903b..a0a16f1 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -224,8 +224,6 @@
* This is used by Gaming and VR applications to ensure the device provides
* will provide consistent performance over a large amount of time.
* </p>
- *
- * {@hide}
*/
public static final int SUSTAINED_PERFORMANCE_WAKE_LOCK = 0x00000100;
diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java
index c028e15..7b0d2a4 100644
--- a/core/java/android/os/storage/StorageVolume.java
+++ b/core/java/android/os/storage/StorageVolume.java
@@ -315,27 +315,34 @@
* To gain access to descendants (child, grandchild, etc) documents, use
* {@link DocumentsContract#buildDocumentUriUsingTree(Uri, String)}, or
* {@link DocumentsContract#buildChildDocumentsUriUsingTree(Uri, String)} with the returned URI.
- *
- * <b>If your application only needs to store internal data, consider using
+ * <p>
+ * If your application only needs to store internal data, consider using
* {@link Context#getExternalFilesDirs(String) Context.getExternalFilesDirs},
- * {@link Context#getExternalCacheDirs()}, or
- * {@link Context#getExternalMediaDirs()}, which require no permissions to read or write.
+ * {@link Context#getExternalCacheDirs()}, or {@link Context#getExternalMediaDirs()}, which
+ * require no permissions to read or write.
+ * <p>
+ * Access to the entire volume is only available for non-primary volumes (for the primary
+ * volume, apps can use the {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} and
+ * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permissions) and should be used
+ * with caution, since users are more likely to deny access when asked for entire volume access
+ * rather than specific directories.
*
- * <strong>NOTE: </strong>requesting access to the entire volume is not recommended and it will
- * result in a stronger message displayed to the user, which may cause the user to reject
- * the request.
- *
- * @param directoryName must be one of
- * {@link Environment#DIRECTORY_MUSIC}, {@link Environment#DIRECTORY_PODCASTS},
- * {@link Environment#DIRECTORY_RINGTONES}, {@link Environment#DIRECTORY_ALARMS},
- * {@link Environment#DIRECTORY_NOTIFICATIONS}, {@link Environment#DIRECTORY_PICTURES},
- * {@link Environment#DIRECTORY_MOVIES}, {@link Environment#DIRECTORY_DOWNLOADS},
- * {@link Environment#DIRECTORY_DCIM}, or {@link Environment#DIRECTORY_DOCUMENTS}, or
- * {code null} to request access to the entire volume.
- *
+ * @param directoryName must be one of {@link Environment#DIRECTORY_MUSIC},
+ * {@link Environment#DIRECTORY_PODCASTS}, {@link Environment#DIRECTORY_RINGTONES},
+ * {@link Environment#DIRECTORY_ALARMS}, {@link Environment#DIRECTORY_NOTIFICATIONS},
+ * {@link Environment#DIRECTORY_PICTURES}, {@link Environment#DIRECTORY_MOVIES},
+ * {@link Environment#DIRECTORY_DOWNLOADS}, {@link Environment#DIRECTORY_DCIM}, or
+ * {@link Environment#DIRECTORY_DOCUMENTS}, or {code null} to request access to the
+ * entire volume.
+ * @return intent to request access, or {@code null} if the requested directory is invalid for
+ * that volume.
* @see DocumentsContract
*/
- public Intent createAccessIntent(String directoryName) {
+ public @Nullable Intent createAccessIntent(String directoryName) {
+ if ((isPrimary() && directoryName == null) ||
+ (directoryName != null && !Environment.isStandardDirectory(directoryName))) {
+ return null;
+ }
final Intent intent = new Intent(ACTION_OPEN_EXTERNAL_DIRECTORY);
intent.putExtra(EXTRA_STORAGE_VOLUME, this);
intent.putExtra(EXTRA_DIRECTORY_NAME, directoryName);
diff --git a/core/java/android/print/IPrintManager.aidl b/core/java/android/print/IPrintManager.aidl
index 5eb8cc2..d7c267b 100644
--- a/core/java/android/print/IPrintManager.aidl
+++ b/core/java/android/print/IPrintManager.aidl
@@ -24,9 +24,11 @@
import android.print.PrintJobId;
import android.print.IPrintJobStateChangeListener;
import android.print.IPrintServicesChangeListener;
+import android.printservice.recommendation.IRecommendationsChangeListener;
import android.print.PrinterId;
import android.print.PrintJobInfo;
import android.print.PrintAttributes;
+import android.printservice.recommendation.RecommendationInfo;
import android.printservice.PrintServiceInfo;
/**
@@ -73,7 +75,6 @@
* Get the print services.
*
* @param selectionFlags flags selecting which services to get
- * @param selectedService if not null, the id of the print service to get
* @param userId the id of the user requesting the services
*
* @return the list of selected print services.
@@ -89,6 +90,37 @@
*/
void setPrintServiceEnabled(in ComponentName service, boolean isEnabled, int userId);
+ /**
+ * Listen for changes to the print service recommendations.
+ *
+ * @param listener the listener to add
+ * @param userId the id of the user listening
+ *
+ * @see android.print.PrintManager#getPrintServiceRecommendations
+ */
+ void addPrintServiceRecommendationsChangeListener(in IRecommendationsChangeListener listener,
+ int userId);
+
+ /**
+ * Stop listening for changes to the print service recommendations.
+ *
+ * @param listener the listener to remove
+ * @param userId the id of the user requesting the removal
+ *
+ * @see android.print.PrintManager#getPrintServiceRecommendations
+ */
+ void removePrintServiceRecommendationsChangeListener(in IRecommendationsChangeListener listener,
+ int userId);
+
+ /**
+ * Get the print service recommendations.
+ *
+ * @param userId the id of the user requesting the recommendations
+ *
+ * @return the list of selected print services.
+ */
+ List<RecommendationInfo> getPrintServiceRecommendations(int userId);
+
void createPrinterDiscoverySession(in IPrinterDiscoveryObserver observer, int userId);
void startPrinterDiscovery(in IPrinterDiscoveryObserver observer,
in List<PrinterId> priorityList, int userId);
diff --git a/core/java/android/print/PageRange.java b/core/java/android/print/PageRange.java
index 57c7718..2941283 100644
--- a/core/java/android/print/PageRange.java
+++ b/core/java/android/print/PageRange.java
@@ -32,6 +32,9 @@
*/
public static final PageRange ALL_PAGES = new PageRange(0, Integer.MAX_VALUE);
+ /** @hide */
+ public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[]{PageRange.ALL_PAGES};
+
private final int mStart;
private final int mEnd;
diff --git a/core/java/android/print/PrintManager.java b/core/java/android/print/PrintManager.java
index 25fc968..71f0bd6 100644
--- a/core/java/android/print/PrintManager.java
+++ b/core/java/android/print/PrintManager.java
@@ -36,12 +36,15 @@
import android.print.PrintDocumentAdapter.LayoutResultCallback;
import android.print.PrintDocumentAdapter.WriteResultCallback;
import android.printservice.PrintServiceInfo;
+import android.printservice.recommendation.IRecommendationsChangeListener;
+import android.printservice.recommendation.RecommendationInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import com.android.internal.os.SomeArgs;
+import com.android.internal.util.Preconditions;
import libcore.io.IoUtils;
import java.lang.ref.WeakReference;
@@ -113,6 +116,7 @@
private static final int MSG_NOTIFY_PRINT_JOB_STATE_CHANGED = 1;
private static final int MSG_NOTIFY_PRINT_SERVICES_CHANGED = 2;
+ private static final int MSG_NOTIFY_PRINT_SERVICE_RECOMMENDATIONS_CHANGED = 3;
/**
* Package name of print spooler.
@@ -202,6 +206,9 @@
mPrintJobStateChangeListeners;
private Map<PrintServicesChangeListener, PrintServicesChangeListenerWrapper>
mPrintServicesChangeListeners;
+ private Map<PrintServiceRecommendationsChangeListener,
+ PrintServiceRecommendationsChangeListenerWrapper>
+ mPrintServiceRecommendationsChangeListeners;
/** @hide */
public interface PrintJobStateChangeListener {
@@ -223,6 +230,15 @@
public void onPrintServicesChanged();
}
+ /** @hide */
+ public interface PrintServiceRecommendationsChangeListener {
+
+ /**
+ * Callback notifying that the print service recommendations changed.
+ */
+ void onPrintServiceRecommendationsChanged();
+ }
+
/**
* Creates a new instance.
*
@@ -260,7 +276,14 @@
listener.onPrintServicesChanged();
}
} break;
-
+ case MSG_NOTIFY_PRINT_SERVICE_RECOMMENDATIONS_CHANGED: {
+ PrintServiceRecommendationsChangeListenerWrapper wrapper =
+ (PrintServiceRecommendationsChangeListenerWrapper) message.obj;
+ PrintServiceRecommendationsChangeListener listener = wrapper.getListener();
+ if (listener != null) {
+ listener.onPrintServiceRecommendationsChanged();
+ }
+ } break;
}
}
};
@@ -539,13 +562,14 @@
* @see android.print.PrintManager#getPrintServices
*/
void addPrintServicesChangeListener(@NonNull PrintServicesChangeListener listener) {
+ Preconditions.checkNotNull(listener);
+
if (mService == null) {
Log.w(LOG_TAG, "Feature android.software.print not available");
return;
}
if (mPrintServicesChangeListeners == null) {
- mPrintServicesChangeListeners = new ArrayMap<PrintServicesChangeListener,
- PrintServicesChangeListenerWrapper>();
+ mPrintServicesChangeListeners = new ArrayMap<>();
}
PrintServicesChangeListenerWrapper wrappedListener =
new PrintServicesChangeListenerWrapper(listener, mHandler);
@@ -565,6 +589,8 @@
* @see android.print.PrintManager#getPrintServices
*/
void removePrintServicesChangeListener(@NonNull PrintServicesChangeListener listener) {
+ Preconditions.checkNotNull(listener);
+
if (mService == null) {
Log.w(LOG_TAG, "Feature android.software.print not available");
return;
@@ -588,7 +614,6 @@
}
}
-
/**
* Gets the list of print services, but does not register for updates. The user has to register
* for updates by itself, or use {@link PrintServicesLoader}.
@@ -596,7 +621,7 @@
* @param selectionFlags flags selecting which services to get. Either
* {@link #ENABLED_SERVICES},{@link #DISABLED_SERVICES}, or both.
*
- * @return The enabled service list or an empty list.
+ * @return The print service list or an empty list.
*
* @see #addPrintServicesChangeListener(PrintServicesChangeListener)
* @see #removePrintServicesChangeListener(PrintServicesChangeListener)
@@ -604,6 +629,8 @@
* @hide
*/
public @NonNull List<PrintServiceInfo> getPrintServices(int selectionFlags) {
+ Preconditions.checkFlagsArgument(selectionFlags, ALL_SERVICES);
+
try {
List<PrintServiceInfo> services = mService.getPrintServices(selectionFlags, mUserId);
if (services != null) {
@@ -616,6 +643,92 @@
}
/**
+ * Listen for changes to the print service recommendations.
+ *
+ * @param listener the listener to add
+ *
+ * @see android.print.PrintManager#getPrintServiceRecommendations
+ */
+ void addPrintServiceRecommendationsChangeListener(
+ @NonNull PrintServiceRecommendationsChangeListener listener) {
+ Preconditions.checkNotNull(listener);
+
+ if (mService == null) {
+ Log.w(LOG_TAG, "Feature android.software.print not available");
+ return;
+ }
+ if (mPrintServiceRecommendationsChangeListeners == null) {
+ mPrintServiceRecommendationsChangeListeners = new ArrayMap<>();
+ }
+ PrintServiceRecommendationsChangeListenerWrapper wrappedListener =
+ new PrintServiceRecommendationsChangeListenerWrapper(listener, mHandler);
+ try {
+ mService.addPrintServiceRecommendationsChangeListener(wrappedListener, mUserId);
+ mPrintServiceRecommendationsChangeListeners.put(listener, wrappedListener);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Stop listening for changes to the print service recommendations.
+ *
+ * @param listener the listener to remove
+ *
+ * @see android.print.PrintManager#getPrintServiceRecommendations
+ */
+ void removePrintServiceRecommendationsChangeListener(
+ @NonNull PrintServiceRecommendationsChangeListener listener) {
+ Preconditions.checkNotNull(listener);
+
+ if (mService == null) {
+ Log.w(LOG_TAG, "Feature android.software.print not available");
+ return;
+ }
+ if (mPrintServiceRecommendationsChangeListeners == null) {
+ return;
+ }
+ PrintServiceRecommendationsChangeListenerWrapper wrappedListener =
+ mPrintServiceRecommendationsChangeListeners.remove(listener);
+ if (wrappedListener == null) {
+ return;
+ }
+ if (mPrintServiceRecommendationsChangeListeners.isEmpty()) {
+ mPrintServiceRecommendationsChangeListeners = null;
+ }
+ wrappedListener.destroy();
+ try {
+ mService.removePrintServiceRecommendationsChangeListener(wrappedListener, mUserId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the list of print service recommendations, but does not register for updates. The user
+ * has to register for updates by itself, or use {@link PrintServiceRecommendationsLoader}.
+ *
+ * @return The print service recommendations list or an empty list.
+ *
+ * @see #addPrintServiceRecommendationsChangeListener
+ * @see #removePrintServiceRecommendationsChangeListener
+ *
+ * @hide
+ */
+ public @NonNull List<RecommendationInfo> getPrintServiceRecommendations() {
+ try {
+ List<RecommendationInfo> recommendations =
+ mService.getPrintServiceRecommendations(mUserId);
+ if (recommendations != null) {
+ return recommendations;
+ }
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ return Collections.emptyList();
+ }
+
+ /**
* @hide
*/
public PrinterDiscoverySession createPrinterDiscoverySession() {
@@ -1242,4 +1355,37 @@
return mWeakListener.get();
}
}
+
+ /**
+ * @hide
+ */
+ public static final class PrintServiceRecommendationsChangeListenerWrapper extends
+ IRecommendationsChangeListener.Stub {
+ private final WeakReference<PrintServiceRecommendationsChangeListener> mWeakListener;
+ private final WeakReference<Handler> mWeakHandler;
+
+ public PrintServiceRecommendationsChangeListenerWrapper(
+ PrintServiceRecommendationsChangeListener listener, Handler handler) {
+ mWeakListener = new WeakReference<>(listener);
+ mWeakHandler = new WeakReference<>(handler);
+ }
+
+ @Override
+ public void onRecommendationsChanged() {
+ Handler handler = mWeakHandler.get();
+ PrintServiceRecommendationsChangeListener listener = mWeakListener.get();
+ if (handler != null && listener != null) {
+ handler.obtainMessage(MSG_NOTIFY_PRINT_SERVICE_RECOMMENDATIONS_CHANGED,
+ this).sendToTarget();
+ }
+ }
+
+ public void destroy() {
+ mWeakListener.clear();
+ }
+
+ public PrintServiceRecommendationsChangeListener getListener() {
+ return mWeakListener.get();
+ }
+ }
}
diff --git a/core/java/android/print/PrintServiceRecommendationsLoader.java b/core/java/android/print/PrintServiceRecommendationsLoader.java
new file mode 100644
index 0000000..bb5d065
--- /dev/null
+++ b/core/java/android/print/PrintServiceRecommendationsLoader.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016 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.print;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Loader;
+import android.os.Handler;
+import android.os.Message;
+import android.printservice.recommendation.RecommendationInfo;
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * Loader for the list of print service recommendations.
+ *
+ * @hide
+ */
+public class PrintServiceRecommendationsLoader extends Loader<List<RecommendationInfo>> {
+ /** The print manager to be used by this object */
+ private final @NonNull PrintManager mPrintManager;
+
+ /** Handler to sequentialize the delivery of the results to the main thread */
+ private final Handler mHandler;
+
+ /** Listens for updates to the data from the platform */
+ private PrintManager.PrintServiceRecommendationsChangeListener mListener;
+
+ /**
+ * Create a new PrintServicesLoader.
+ *
+ * @param printManager The print manager supplying the data
+ * @param context Context of the using object
+ */
+ public PrintServiceRecommendationsLoader(@NonNull PrintManager printManager,
+ @NonNull Context context) {
+ super(Preconditions.checkNotNull(context));
+ mHandler = new MyHandler();
+ mPrintManager = Preconditions.checkNotNull(printManager);
+ }
+
+ @Override
+ protected void onForceLoad() {
+ queueNewResult();
+ }
+
+ /**
+ * Read the print service recommendations and queue it to be delivered on the main thread.
+ */
+ private void queueNewResult() {
+ Message m = mHandler.obtainMessage(0);
+ m.obj = mPrintManager.getPrintServiceRecommendations();
+ mHandler.sendMessage(m);
+ }
+
+ @Override
+ protected void onStartLoading() {
+ mListener = new PrintManager.PrintServiceRecommendationsChangeListener() {
+ @Override
+ public void onPrintServiceRecommendationsChanged() {
+ queueNewResult();
+ }
+ };
+
+ mPrintManager.addPrintServiceRecommendationsChangeListener(mListener);
+
+ // Immediately deliver a result
+ deliverResult(mPrintManager.getPrintServiceRecommendations());
+ }
+
+ @Override
+ protected void onStopLoading() {
+ if (mListener != null) {
+ mPrintManager.removePrintServiceRecommendationsChangeListener(mListener);
+ mListener = null;
+ }
+
+ if (mHandler != null) {
+ mHandler.removeMessages(0);
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ onStopLoading();
+ }
+
+ /**
+ * Handler to sequentialize all the updates to the main thread.
+ */
+ private class MyHandler extends Handler {
+ /**
+ * Create a new handler on the main thread.
+ */
+ public MyHandler() {
+ super(getContext().getMainLooper());
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (isStarted()) {
+ deliverResult((List<RecommendationInfo>) msg.obj);
+ }
+ }
+ }
+}
diff --git a/core/java/android/print/PrintServicesLoader.java b/core/java/android/print/PrintServicesLoader.java
index ed41114..60d7d66 100644
--- a/core/java/android/print/PrintServicesLoader.java
+++ b/core/java/android/print/PrintServicesLoader.java
@@ -22,6 +22,7 @@
import android.os.Handler;
import android.os.Message;
import android.printservice.PrintServiceInfo;
+import com.android.internal.util.Preconditions;
import java.util.List;
@@ -46,13 +47,16 @@
/**
* Create a new PrintServicesLoader.
*
+ * @param printManager The print manager supplying the data
+ * @param context Context of the using object
* @param selectionFlags What type of services to load.
*/
public PrintServicesLoader(@NonNull PrintManager printManager, @NonNull Context context,
int selectionFlags) {
- super(context);
- mPrintManager = printManager;
- mSelectionFlags = selectionFlags;
+ super(Preconditions.checkNotNull(context));
+ mPrintManager = Preconditions.checkNotNull(printManager);
+ mSelectionFlags = Preconditions.checkFlagsArgument(selectionFlags,
+ PrintManager.ALL_SERVICES);
}
@Override
diff --git a/core/java/android/printservice/recommendation/IRecommendationService.aidl b/core/java/android/printservice/recommendation/IRecommendationService.aidl
new file mode 100644
index 0000000..ce9ea6f
--- /dev/null
+++ b/core/java/android/printservice/recommendation/IRecommendationService.aidl
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation;
+
+import android.printservice.recommendation.IRecommendationServiceCallbacks;
+
+/**
+ * Interface for communication with the print service recommendation service.
+ *
+ * @see android.print.IPrintServiceRecommendationServiceCallbacks
+ *
+ * @hide
+ */
+oneway interface IRecommendationService {
+ void registerCallbacks(in IRecommendationServiceCallbacks callbacks);
+}
diff --git a/core/java/android/printservice/recommendation/IRecommendationServiceCallbacks.aidl b/core/java/android/printservice/recommendation/IRecommendationServiceCallbacks.aidl
new file mode 100644
index 0000000..9528654
--- /dev/null
+++ b/core/java/android/printservice/recommendation/IRecommendationServiceCallbacks.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation;
+
+import android.printservice.recommendation.RecommendationInfo;
+
+/**
+ * Callbacks for communication with the print service recommendation service.
+ *
+ * @see android.print.IPrintServiceRecommendationService
+ *
+ * @hide
+ */
+oneway interface IRecommendationServiceCallbacks {
+ /**
+ * Update the print service recommendations.
+ *
+ * @param recommendations the new print service recommendations
+ */
+ void onRecommendationsUpdated(in List<RecommendationInfo> recommendations);
+}
diff --git a/core/java/android/printservice/recommendation/IRecommendationsChangeListener.aidl b/core/java/android/printservice/recommendation/IRecommendationsChangeListener.aidl
new file mode 100644
index 0000000..8ca5c69
--- /dev/null
+++ b/core/java/android/printservice/recommendation/IRecommendationsChangeListener.aidl
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation;
+
+/**
+ * Interface for observing changes of the print service recommendations.
+ *
+ * @hide
+ */
+oneway interface IRecommendationsChangeListener {
+ void onRecommendationsChanged();
+}
diff --git a/core/java/android/printservice/recommendation/RecommendationInfo.aidl b/core/java/android/printservice/recommendation/RecommendationInfo.aidl
new file mode 100644
index 0000000..f21d0bf
--- /dev/null
+++ b/core/java/android/printservice/recommendation/RecommendationInfo.aidl
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2016, 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.printservice.recommendation;
+
+/**
+ * @hide
+ */
+parcelable RecommendationInfo;
diff --git a/core/java/android/printservice/recommendation/RecommendationInfo.java b/core/java/android/printservice/recommendation/RecommendationInfo.java
new file mode 100644
index 0000000..65d534e
--- /dev/null
+++ b/core/java/android/printservice/recommendation/RecommendationInfo.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.printservice.PrintService;
+import com.android.internal.util.Preconditions;
+
+/**
+ * A recommendation to install a {@link PrintService print service}.
+ *
+ * @hide
+ */
+@SystemApi
+public final class RecommendationInfo implements Parcelable {
+ /** Package name of the print service. */
+ private @NonNull final CharSequence mPackageName;
+
+ /** Display name of the print service. */
+ private @NonNull final CharSequence mName;
+
+ /** Number of printers the print service would discover if installed. */
+ private @IntRange(from = 0) final int mNumDiscoveredPrinters;
+
+ /** If the service detects printer from multiple vendors. */
+ private final boolean mRecommendsMultiVendorService;
+
+ /**
+ * Create a new recommendation.
+ *
+ * @param packageName Package name of the print service
+ * @param name Display name of the print service
+ * @param numDiscoveredPrinters Number of printers the print service would discover if
+ * installed
+ * @param recommendsMultiVendorService If the service detects printer from multiple vendor
+ */
+ public RecommendationInfo(@NonNull CharSequence packageName, @NonNull CharSequence name,
+ @IntRange(from = 0) int numDiscoveredPrinters, boolean recommendsMultiVendorService) {
+ mPackageName = Preconditions.checkStringNotEmpty(packageName);
+ mName = Preconditions.checkStringNotEmpty(name);
+ mNumDiscoveredPrinters = Preconditions.checkArgumentNonnegative(numDiscoveredPrinters);
+ mRecommendsMultiVendorService = recommendsMultiVendorService;
+ }
+
+ /**
+ * Create a new recommendation from a parcel.
+ *
+ * @param parcel The parcel containing the data
+ *
+ * @see #CREATOR
+ */
+ private RecommendationInfo(@NonNull Parcel parcel) {
+ this(parcel.readCharSequence(), parcel.readCharSequence(), parcel.readInt(),
+ parcel.readByte() != 0);
+ }
+
+ /**
+ * @return The package name the recommendations recommends.
+ */
+ public CharSequence getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * @return Whether the recommended print service detects printers of more than one vendor.
+ */
+ public boolean recommendsMultiVendorService() {
+ return mRecommendsMultiVendorService;
+ }
+
+ /**
+ * @return The number of printer the print service would detect.
+ */
+ public int getNumDiscoveredPrinters() {
+ return mNumDiscoveredPrinters;
+ }
+
+ /**
+ * @return The name of the recommended print service.
+ */
+ public CharSequence getName() {
+ return mName;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequence(mPackageName);
+ dest.writeCharSequence(mName);
+ dest.writeInt(mNumDiscoveredPrinters);
+ dest.writeByte((byte) (mRecommendsMultiVendorService ? 1 : 0));
+ }
+
+ /**
+ * Utility class used to create new print service recommendation objects from parcels.
+ *
+ * @see #RecommendationInfo(Parcel)
+ */
+ public static final Creator<RecommendationInfo> CREATOR =
+ new Creator<RecommendationInfo>() {
+ @Override
+ public RecommendationInfo createFromParcel(Parcel in) {
+ return new RecommendationInfo(in);
+ }
+
+ @Override
+ public RecommendationInfo[] newArray(int size) {
+ return new RecommendationInfo[size];
+ }
+ };
+}
diff --git a/core/java/android/printservice/recommendation/RecommendationService.java b/core/java/android/printservice/recommendation/RecommendationService.java
new file mode 100644
index 0000000..b7ea512
--- /dev/null
+++ b/core/java/android/printservice/recommendation/RecommendationService.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.List;
+
+/**
+ * Base class for the print service recommendation services.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class RecommendationService extends Service {
+ private static final String LOG_TAG = "PrintServiceRecS";
+
+ /** Used to push onConnect and onDisconnect on the main thread */
+ private Handler mHandler;
+
+ /**
+ * The {@link Intent} action that must be declared as handled by a service in its manifest for
+ * the system to recognize it as a print service recommendation service.
+ *
+ * @hide
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.printservice.recommendation.RecommendationService";
+
+ /** Registered callbacks, only modified on main thread */
+ private IRecommendationServiceCallbacks mCallbacks;
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+
+ mHandler = new MyHandler();
+ }
+
+ /**
+ * Update the print service recommendations.
+ *
+ * @param recommendations The new set of recommendations
+ */
+ public final void updateRecommendations(@Nullable List<RecommendationInfo> recommendations) {
+ mHandler.obtainMessage(MyHandler.MSG_UPDATE, recommendations).sendToTarget();
+ }
+
+ @Override
+ public final IBinder onBind(Intent intent) {
+ return new IRecommendationService.Stub() {
+ @Override
+ public void registerCallbacks(IRecommendationServiceCallbacks callbacks) {
+ // The callbacks come in order of the caller on oneway calls. Hence while the caller
+ // cannot know at what time the connection is made, he can know the ordering of
+ // connection and disconnection.
+ //
+ // Similar he cannot know when the disconnection is processed, hence he has to
+ // handle callbacks after calling disconnect.
+ if (callbacks != null) {
+ mHandler.obtainMessage(MyHandler.MSG_CONNECT, callbacks).sendToTarget();
+ } else {
+ mHandler.obtainMessage(MyHandler.MSG_DISCONNECT).sendToTarget();
+ }
+ }
+ };
+ }
+
+ /**
+ * Called when the client connects to the recommendation service.
+ */
+ public abstract void onConnected();
+
+ /**
+ * Called when the client disconnects from the recommendation service.
+ */
+ public abstract void onDisconnected();
+
+ private class MyHandler extends Handler {
+ static final int MSG_CONNECT = 1;
+ static final int MSG_DISCONNECT = 2;
+ static final int MSG_UPDATE = 3;
+
+ MyHandler() {
+ super(Looper.getMainLooper());
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_CONNECT:
+ mCallbacks = (IRecommendationServiceCallbacks) msg.obj;
+ onConnected();
+ break;
+ case MSG_DISCONNECT:
+ onDisconnected();
+ mCallbacks = null;
+ break;
+ case MSG_UPDATE:
+ // Note that there might be a connection change in progress. In this case the
+ // message is handled as before the change. This is acceptable as the caller of
+ // the connection change has not guarantee when the connection change binder
+ // transaction is actually processed.
+ try {
+ mCallbacks.onRecommendationsUpdated((List<RecommendationInfo>) msg.obj);
+ } catch (RemoteException | NullPointerException e) {
+ Log.e(LOG_TAG, "Could not update recommended services", e);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index a78f468..ebecfdb 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7679,6 +7679,9 @@
BLUETOOTH_MAP_PRIORITY_PREFIX = "bluetooth_map_priority_";
/** {@hide} */
public static final String
+ BLUETOOTH_PBAP_CLIENT_PRIORITY_PREFIX = "bluetooth_pbap_client_priority_";
+ /** {@hide} */
+ public static final String
BLUETOOTH_SAP_PRIORITY_PREFIX = "bluetooth_sap_priority_";
/**
@@ -7834,6 +7837,14 @@
}
/**
+ * Get the key that retrieves a bluetooth pbap client priority.
+ * @hide
+ */
+ public static final String getBluetoothPbapClientPriorityKey(String address) {
+ return BLUETOOTH_PBAP_CLIENT_PRIORITY_PREFIX + address.toUpperCase(Locale.ROOT);
+ }
+
+ /**
* Get the key that retrieves a bluetooth map priority.
* @hide
*/
diff --git a/core/java/android/util/Pair.java b/core/java/android/util/Pair.java
index 6027d08..f96da72 100644
--- a/core/java/android/util/Pair.java
+++ b/core/java/android/util/Pair.java
@@ -16,7 +16,7 @@
package android.util;
-import libcore.util.Objects;
+import java.util.Objects;
/**
* Container to ease passing around a tuple of two objects. This object provides a sensible
@@ -52,7 +52,7 @@
return false;
}
Pair<?, ?> p = (Pair<?, ?>) o;
- return Objects.equal(p.first, first) && Objects.equal(p.second, second);
+ return Objects.equals(p.first, first) && Objects.equals(p.second, second);
}
/**
@@ -65,6 +65,11 @@
return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode());
}
+ @Override
+ public String toString() {
+ return "Pair{" + String.valueOf(first) + " " + String.valueOf(second) + "}";
+ }
+
/**
* Convenience method for creating an appropriately typed pair.
* @param a the first object in the Pair
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 6811aed..dd79e62 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -16299,8 +16299,10 @@
/**
* Create a snapshot of the view into a bitmap. We should probably make
* some form of this public, but should think about the API.
+ *
+ * @hide
*/
- Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
+ public Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
int width = mRight - mLeft;
int height = mBottom - mTop;
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index f695873..3f7bbdf 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -3251,8 +3251,11 @@
}
}
+ /**
+ * @hide
+ */
@Override
- Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
+ public Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
int count = mChildrenCount;
int[] visibilities = null;
@@ -3262,7 +3265,8 @@
View child = getChildAt(i);
visibilities[i] = child.getVisibility();
if (visibilities[i] == View.VISIBLE) {
- child.setVisibility(INVISIBLE);
+ child.mViewFlags = (child.mViewFlags & ~View.VISIBILITY_MASK)
+ | (View.INVISIBLE & View.VISIBILITY_MASK);
}
}
}
@@ -3271,7 +3275,9 @@
if (skipChildren) {
for (int i = 0; i < count; i++) {
- getChildAt(i).setVisibility(visibilities[i]);
+ View child = getChildAt(i);
+ child.mViewFlags = (child.mViewFlags & ~View.VISIBILITY_MASK)
+ | (visibilities[i] & View.VISIBILITY_MASK);
}
}
diff --git a/core/java/android/view/ViewPropertyAnimator.java b/core/java/android/view/ViewPropertyAnimator.java
index c5b8849..f18b7ac 100644
--- a/core/java/android/view/ViewPropertyAnimator.java
+++ b/core/java/android/view/ViewPropertyAnimator.java
@@ -1113,13 +1113,6 @@
if (mListener != null) {
mListener.onAnimationEnd(animation);
}
- if (mAnimatorCleanupMap != null) {
- Runnable r = mAnimatorCleanupMap.get(animation);
- if (r != null) {
- r.run();
- }
- mAnimatorCleanupMap.remove(animation);
- }
if (mAnimatorOnEndMap != null) {
Runnable r = mAnimatorOnEndMap.get(animation);
if (r != null) {
@@ -1127,6 +1120,13 @@
}
mAnimatorOnEndMap.remove(animation);
}
+ if (mAnimatorCleanupMap != null) {
+ Runnable r = mAnimatorCleanupMap.get(animation);
+ if (r != null) {
+ r.run();
+ }
+ mAnimatorCleanupMap.remove(animation);
+ }
mAnimatorMap.remove(animation);
}
diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java
index 36ee3e6..2f3f0bf 100644
--- a/core/java/android/view/Window.java
+++ b/core/java/android/view/Window.java
@@ -298,7 +298,7 @@
private boolean mDestroyed;
- private boolean mOverlayWithDecorCaption = false;
+ private boolean mOverlayWithDecorCaptionEnabled = false;
// The current window attributes.
private final WindowManager.LayoutParams mWindowAttributes =
@@ -2139,13 +2139,13 @@
* down. This affects only freeform windows since they display the caption.
* @hide
*/
- public void setOverlayDecorCaption(boolean overlayCaption) {
- mOverlayWithDecorCaption = overlayCaption;
+ public void setOverlayWithDecorCaptionEnabled(boolean enabled) {
+ mOverlayWithDecorCaptionEnabled = enabled;
}
/** @hide */
- public boolean getOverlayDecorCaption() {
- return mOverlayWithDecorCaption;
+ public boolean isOverlayWithDecorCaptionEnabled() {
+ return mOverlayWithDecorCaptionEnabled;
}
/** @hide */
@@ -2181,7 +2181,7 @@
* Called when the activity changes from fullscreen mode to multi-window mode and visa-versa.
* @hide
*/
- public abstract void onMultiWindowChanged();
+ public abstract void onMultiWindowModeChanged();
/**
* Called when the activity just relaunched.
diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java
index c1392fe..23b0df2 100644
--- a/core/java/android/view/WindowManagerPolicy.java
+++ b/core/java/android/view/WindowManagerPolicy.java
@@ -415,7 +415,7 @@
* Returns true if the window is current in multi-windowing mode. i.e. it shares the
* screen with other application windows.
*/
- public boolean inMultiWindowMode();
+ public boolean isInMultiWindowMode();
}
/**
diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java
index a3c49c5..89dec2d 100644
--- a/core/java/android/view/inputmethod/BaseInputConnection.java
+++ b/core/java/android/view/inputmethod/BaseInputConnection.java
@@ -16,6 +16,7 @@
package android.view.inputmethod;
+import android.annotation.CallSuper;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
@@ -154,12 +155,11 @@
}
/**
- * Called when this InputConnection is no longer used by the InputMethodManager.
- *
- * @hide
+ * Default implementation calls {@link #finishComposingText()}.
*/
- public void reportFinish() {
- // Intentionally empty
+ @CallSuper
+ public void closeConnection() {
+ finishComposingText();
}
/**
diff --git a/core/java/android/view/inputmethod/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java
index 8002a8e..9f66429 100644
--- a/core/java/android/view/inputmethod/InputConnection.java
+++ b/core/java/android/view/inputmethod/InputConnection.java
@@ -45,6 +45,8 @@
* was introduced in {@link android.os.Build.VERSION_CODES#N}.</li>
* <li>{@link #getHandler()}}, which was introduced in
* {@link android.os.Build.VERSION_CODES#N}.</li>
+ * <li>{@link #closeConnection()}}, which was introduced in
+ * {@link android.os.Build.VERSION_CODES#N}.</li>
* </ul>
*
* <h3>Implementing an IME or an editor</h3>
@@ -820,4 +822,18 @@
* @return {@code null} to use the default {@link Handler}.
*/
public Handler getHandler();
+
+ /**
+ * Called by the system up to only once to notify that the system is about to invalidate
+ * connection between the input method and the application.
+ *
+ * <p><strong>Editor authors</strong>: You can clear all the nested batch edit right now and
+ * you no longer need to handle subsequent callbacks on this connection, including
+ * {@link #beginBatchEdit()}}. Note that although the system tries to call this method whenever
+ * possible, there may be a chance that this method is not called in some exceptional
+ * situations.</p>
+ *
+ * <p>Note: This does nothing when called from input methods.</p>
+ */
+ public void closeConnection();
}
diff --git a/core/java/android/view/inputmethod/InputConnectionInspector.java b/core/java/android/view/inputmethod/InputConnectionInspector.java
index 46b2c3e..118a61f 100644
--- a/core/java/android/view/inputmethod/InputConnectionInspector.java
+++ b/core/java/android/view/inputmethod/InputConnectionInspector.java
@@ -73,6 +73,11 @@
* {@link android.os.Build.VERSION_CODES#N} and later.
*/
int GET_HANDLER = 1 << 5;
+ /**
+ * {@link InputConnection#closeConnection()}} is available in
+ * {@link android.os.Build.VERSION_CODES#N} and later.
+ */
+ int CLOSE_CONNECTION = 1 << 6;
}
private static final Map<Class, Integer> sMissingMethodsMap = Collections.synchronizedMap(
@@ -119,6 +124,9 @@
if (!hasGetHandler(clazz)) {
flags |= MissingMethodFlags.GET_HANDLER;
}
+ if (!hasCloseConnection(clazz)) {
+ flags |= MissingMethodFlags.CLOSE_CONNECTION;
+ }
sMissingMethodsMap.put(clazz, flags);
return flags;
}
@@ -178,6 +186,15 @@
}
}
+ private static boolean hasCloseConnection(@NonNull final Class clazz) {
+ try {
+ final Method method = clazz.getMethod("closeConnection");
+ return !Modifier.isAbstract(method.getModifiers());
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
public static String getMissingMethodFlagsAsString(@MissingMethodFlags final int flags) {
final StringBuilder sb = new StringBuilder();
boolean isEmpty = true;
@@ -219,6 +236,12 @@
}
sb.append("getHandler()");
}
+ if ((flags & MissingMethodFlags.CLOSE_CONNECTION) != 0) {
+ if (!isEmpty) {
+ sb.append(",");
+ }
+ sb.append("closeConnection()");
+ }
return sb.toString();
}
}
diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java
index 381df49..e743f62 100644
--- a/core/java/android/view/inputmethod/InputConnectionWrapper.java
+++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java
@@ -261,4 +261,12 @@
public Handler getHandler() {
return mTarget.getHandler();
}
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ public void closeConnection() {
+ mTarget.closeConnection();
+ }
}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index f5908a5..6879901 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -544,7 +544,7 @@
// reportFinish() will take effect.
return;
}
- reportFinish();
+ closeConnection();
}
@Override
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 7cbe8de..6e1dff9 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -5937,6 +5937,11 @@
public Handler getHandler() {
return getTarget().getHandler();
}
+
+ @Override
+ public void closeConnection() {
+ getTarget().closeConnection();
+ }
}
/**
diff --git a/core/java/com/android/internal/app/LocaleHelper.java b/core/java/com/android/internal/app/LocaleHelper.java
index d8d6e56..36db5d7 100644
--- a/core/java/com/android/internal/app/LocaleHelper.java
+++ b/core/java/com/android/internal/app/LocaleHelper.java
@@ -181,6 +181,7 @@
public static final class LocaleInfoComparator implements Comparator<LocaleStore.LocaleInfo> {
private final Collator mCollator;
private final boolean mCountryMode;
+ private static final String PREFIX_ARABIC = "\u0627\u0644"; // ALEF-LAM, ال
/**
* Constructor.
@@ -192,6 +193,20 @@
mCountryMode = countryMode;
}
+ /*
+ * The Arabic collation should ignore Alef-Lam at the beginning (b/26277596)
+ *
+ * We look at the label's locale, not the current system locale.
+ * This is because the name of the Arabic language itself is in Arabic,
+ * and starts with Alef-Lam, no matter what the system locale is.
+ */
+ private String removePrefixForCompare(Locale locale, String str) {
+ if ("ar".equals(locale.getLanguage()) && str.startsWith(PREFIX_ARABIC)) {
+ return str.substring(PREFIX_ARABIC.length());
+ }
+ return str;
+ }
+
/**
* Compares its two arguments for order.
*
@@ -206,7 +221,9 @@
// and "all others" (== 0)
if (mCountryMode || (lhs.isSuggested() == rhs.isSuggested())) {
// They are in the same "bucket" (suggested / others), so we compare the text
- return mCollator.compare(lhs.getLabel(mCountryMode), rhs.getLabel(mCountryMode));
+ return mCollator.compare(
+ removePrefixForCompare(lhs.getLocale(), lhs.getLabel(mCountryMode)),
+ removePrefixForCompare(rhs.getLocale(), rhs.getLabel(mCountryMode)));
} else {
// One locale is suggested and one is not, so we put them in different "buckets"
return lhs.isSuggested() ? -1 : 1;
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 794a6d6..d80b63a 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -680,7 +680,7 @@
}
@Override
- public void onMultiWindowChanged() {
+ public void onMultiWindowModeChanged() {
if (mDecor != null) {
mDecor.onConfigurationChanged(getContext().getResources().getConfiguration());
}
diff --git a/core/java/com/android/internal/util/Preconditions.java b/core/java/com/android/internal/util/Preconditions.java
index c46851e..7c85246 100644
--- a/core/java/com/android/internal/util/Preconditions.java
+++ b/core/java/com/android/internal/util/Preconditions.java
@@ -56,7 +56,7 @@
* @return the string reference that was validated
* @throws IllegalArgumentException if {@code string} is empty
*/
- public static @NonNull String checkStringNotEmpty(final String string) {
+ public static @NonNull <T extends CharSequence> T checkStringNotEmpty(final T string) {
if (TextUtils.isEmpty(string)) {
throw new IllegalArgumentException();
}
@@ -73,7 +73,7 @@
* @return the string reference that was validated
* @throws IllegalArgumentException if {@code string} is empty
*/
- public static @NonNull String checkStringNotEmpty(final String string,
+ public static @NonNull <T extends CharSequence> T checkStringNotEmpty(final T string,
final Object errorMessage) {
if (TextUtils.isEmpty(string)) {
throw new IllegalArgumentException(String.valueOf(errorMessage));
@@ -141,13 +141,17 @@
/**
* Check the requested flags, throwing if any requested flags are outside
* the allowed set.
+ *
+ * @return the validated requested flags.
*/
- public static void checkFlagsArgument(final int requestedFlags, final int allowedFlags) {
+ public static int checkFlagsArgument(final int requestedFlags, final int allowedFlags) {
if ((requestedFlags & allowedFlags) != requestedFlags) {
throw new IllegalArgumentException("Requested flags 0x"
+ Integer.toHexString(requestedFlags) + ", but only 0x"
+ Integer.toHexString(allowedFlags) + " are allowed");
}
+
+ return requestedFlags;
}
/**
@@ -170,6 +174,22 @@
/**
* Ensures that that the argument numeric value is non-negative.
*
+ * @param value a numeric int value
+ *
+ * @return the validated numeric value
+ * @throws IllegalArgumentException if {@code value} was negative
+ */
+ public static @IntRange(from = 0) int checkArgumentNonnegative(final int value) {
+ if (value < 0) {
+ throw new IllegalArgumentException();
+ }
+
+ return value;
+ }
+
+ /**
+ * Ensures that that the argument numeric value is non-negative.
+ *
* @param value a numeric long value
* @param errorMessage the exception message to use if the check fails
* @return the validated numeric value
diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java
index 6c1ebb4..3a4afad 100644
--- a/core/java/com/android/internal/view/IInputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java
@@ -27,13 +27,12 @@
import android.os.RemoteException;
import android.util.Log;
import android.view.KeyEvent;
-import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
-
-import java.lang.ref.WeakReference;
+import android.view.inputmethod.InputConnectionInspector;
+import android.view.inputmethod.InputConnectionInspector.MissingMethodFlags;
public abstract class IInputConnectionWrapper extends IInputContext.Stub {
static final String TAG = "IInputConnectionWrapper";
@@ -61,7 +60,7 @@
private static final int DO_PERFORM_PRIVATE_COMMAND = 120;
private static final int DO_CLEAR_META_KEY_STATES = 130;
private static final int DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO = 140;
- private static final int DO_REPORT_FINISH = 150;
+ private static final int DO_CLOSE_CONNECTION = 150;
@GuardedBy("mLock")
@Nullable
@@ -222,8 +221,8 @@
seq, callback));
}
- public void reportFinish() {
- dispatchMessage(obtainMessage(DO_REPORT_FINISH));
+ public void closeConnection() {
+ dispatchMessage(obtainMessage(DO_CLOSE_CONNECTION));
}
void dispatchMessage(Message msg) {
@@ -501,10 +500,10 @@
}
return;
}
- case DO_REPORT_FINISH: {
+ case DO_CLOSE_CONNECTION: {
// Note that we do not need to worry about race condition here, because 1) mFinished
// is updated only inside this block, and 2) the code here is running on a Handler
- // hence we assume multiple DO_REPORT_FINISH messages will not be handled at the
+ // hence we assume multiple DO_CLOSE_CONNECTION messages will not be handled at the
// same time.
if (isFinished()) {
return;
@@ -518,11 +517,10 @@
if (ic == null) {
return;
}
- ic.finishComposingText();
- // TODO: Make reportFinish() public method of InputConnection to remove this
- // check.
- if (ic instanceof BaseInputConnection) {
- ((BaseInputConnection) ic).reportFinish();
+ @MissingMethodFlags
+ final int missingMethods = InputConnectionInspector.getMissingMethodFlags(ic);
+ if ((missingMethods & MissingMethodFlags.CLOSE_CONNECTION) == 0) {
+ ic.closeConnection();
}
} finally {
synchronized (mLock) {
diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java
index 85b8606..f9884d8 100644
--- a/core/java/com/android/internal/view/InputConnectionWrapper.java
+++ b/core/java/com/android/internal/view/InputConnectionWrapper.java
@@ -487,6 +487,10 @@
return null;
}
+ public void closeConnection() {
+ // Nothing should happen when called from input method.
+ }
+
private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) {
return (mMissingMethods & methodFlag) == methodFlag;
}
diff --git a/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java
index 526e2ae..9e2cbdb 100644
--- a/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java
+++ b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java
@@ -45,7 +45,8 @@
private static float[] createLUT(TimeInterpolator interpolator, long duration) {
long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos();
int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS);
- int numAnimFrames = (int) Math.ceil(((double) duration) / animIntervalMs);
+ // We need 2 frame values as the minimal.
+ int numAnimFrames = Math.max(2, (int) Math.ceil(((double) duration) / animIntervalMs));
float values[] = new float[numAnimFrames];
float lastFrame = numAnimFrames - 1;
for (int i = 0; i < numAnimFrames; i++) {
diff --git a/core/java/com/android/internal/widget/DecorCaptionView.java b/core/java/com/android/internal/widget/DecorCaptionView.java
index 409a17f..e59d7ba 100644
--- a/core/java/com/android/internal/widget/DecorCaptionView.java
+++ b/core/java/com/android/internal/widget/DecorCaptionView.java
@@ -136,7 +136,7 @@
public void setPhoneWindow(PhoneWindow owner, boolean show) {
mOwner = owner;
mShow = show;
- mOverlayWithAppContent = owner.getOverlayDecorCaption();
+ mOverlayWithAppContent = owner.isOverlayWithDecorCaptionEnabled();
if (mOverlayWithAppContent) {
// The caption is covering the content, so we make its background transparent to make
// the content visible.
diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java
index a96b5a0..7f7528d 100644
--- a/core/java/com/android/internal/widget/EditableInputConnection.java
+++ b/core/java/com/android/internal/widget/EditableInputConnection.java
@@ -83,13 +83,9 @@
return false;
}
- /**
- * @hide
- */
@Override
- public void reportFinish() {
- super.reportFinish();
-
+ public void closeConnection() {
+ super.closeConnection();
synchronized(this) {
while (mBatchEditNesting > 0) {
endBatchEdit();
diff --git a/core/jni/android_hardware_SensorManager.cpp b/core/jni/android_hardware_SensorManager.cpp
index e39bb1c..90f407f 100644
--- a/core/jni/android_hardware_SensorManager.cpp
+++ b/core/jni/android_hardware_SensorManager.cpp
@@ -36,15 +36,17 @@
#include "core_jni_helpers.h"
-static struct {
+namespace {
+
+using namespace android;
+
+struct {
jclass clazz;
jmethodID dispatchSensorEvent;
jmethodID dispatchFlushCompleteEvent;
jmethodID dispatchAdditionalInfoEvent;
} gBaseEventQueueClassInfo;
-namespace android {
-
struct SensorOffsets
{
jclass clazz;
@@ -72,9 +74,9 @@
} gListOffsets;
/*
- * The method below are not thread-safe and not intended to be
+ * nativeClassInit is not inteneded to be thread-safe. It should be called before other native...
+ * functions (except nativeCreate).
*/
-
static void
nativeClassInit (JNIEnv *_env, jclass _this)
{
@@ -494,9 +496,7 @@
(void*)nativeInjectSensorData },
};
-}; // namespace android
-
-using namespace android;
+} //unnamed namespace
int register_android_hardware_SensorManager(JNIEnv *env)
{
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 8abb7e2..b0fcc28 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2172,6 +2172,15 @@
<permission android:name="android.permission.BIND_PRINT_SERVICE"
android:protectionLevel="signature" />
+ <!-- Must be required by a {@link android.printservice.recommendation.RecommendationService},
+ to ensure that only the system can bind to it.
+ @hide
+ @SystemApi
+ <p>Protection level: signature
+ -->
+ <permission android:name="android.permission.BIND_PRINT_RECOMMENDATION_SERVICE"
+ android:protectionLevel="signature" />
+
<!-- Must be required by a {@link android.nfc.cardemulation.HostApduService}
or {@link android.nfc.cardemulation.OffHostApduService} to ensure that only
the system can bind to it.
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index b780778..b7926cf 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1093,7 +1093,12 @@
</intent-filter>
</activity>
-
+ <activity android:name="android.view.ViewCaptureTestActivity" android:label="ViewCaptureTestActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+ </intent-filter>
+ </activity>
<!-- Activity-level metadata -->
<meta-data android:name="com.android.frameworks.coretests.isApp" android:value="true" />
@@ -1251,6 +1256,15 @@
<service android:name="android.os.BinderThreadPriorityService"
android:process=":BinderThreadPriorityService" />
+ <!-- Used by ApplyOverrideConfigurationTest -->
+ <activity android:name="android.app.activity.ApplyOverrideConfigurationActivity"
+ android:configChanges="orientation|screenSize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+ </intent-filter>
+ </activity>
+
<!-- Application components used for search manager tests -->
<activity android:name="android.app.activity.SearchableActivity"
diff --git a/core/tests/coretests/res/drawable-nodpi/view_capture_test_no_children_golden.png b/core/tests/coretests/res/drawable-nodpi/view_capture_test_no_children_golden.png
new file mode 100644
index 0000000..52092c4
--- /dev/null
+++ b/core/tests/coretests/res/drawable-nodpi/view_capture_test_no_children_golden.png
Binary files differ
diff --git a/core/tests/coretests/res/drawable-nodpi/view_capture_test_with_children_golden.png b/core/tests/coretests/res/drawable-nodpi/view_capture_test_with_children_golden.png
new file mode 100644
index 0000000..3bcbd9a
--- /dev/null
+++ b/core/tests/coretests/res/drawable-nodpi/view_capture_test_with_children_golden.png
Binary files differ
diff --git a/core/tests/coretests/res/layout/view_capture_snapshot.xml b/core/tests/coretests/res/layout/view_capture_snapshot.xml
new file mode 100644
index 0000000..0b589a4
--- /dev/null
+++ b/core/tests/coretests/res/layout/view_capture_snapshot.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2016, 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.
+*/
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout android:id="@+id/capture"
+ android:background="#00f"
+ android:orientation="vertical"
+ android:layout_width="100px"
+ android:layout_height="100px">
+
+ <View android:id="@+id/child1"
+ android:background="#f00"
+ android:layout_width="match_parent"
+ android:layout_height="25px"/>
+
+ <View android:id="@+id/child2"
+ android:background="#fff"
+ android:layout_width="match_parent"
+ android:layout_height="25px"
+ android:visibility="invisible"/>
+
+ <View android:id="@+id/child3"
+ android:background="#ff0"
+ android:layout_width="match_parent"
+ android:layout_height="25px"
+ android:visibility="gone"/>
+
+ <View android:id="@+id/child4"
+ android:background="#0ff"
+ android:layout_width="match_parent"
+ android:layout_height="25px" />
+ </LinearLayout>
+</FrameLayout>
diff --git a/core/tests/coretests/src/android/app/activity/ApplyOverrideConfigurationActivity.java b/core/tests/coretests/src/android/app/activity/ApplyOverrideConfigurationActivity.java
new file mode 100644
index 0000000..3df522d
--- /dev/null
+++ b/core/tests/coretests/src/android/app/activity/ApplyOverrideConfigurationActivity.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 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.app.activity;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+
+public class ApplyOverrideConfigurationActivity extends Activity {
+
+ @Override
+ protected void attachBaseContext(Context newBase) {
+ super.attachBaseContext(newBase);
+
+ Configuration overrideConfig = new Configuration();
+ overrideConfig.smallestScreenWidthDp = ApplyOverrideConfigurationTest.OVERRIDE_WIDTH;
+ applyOverrideConfiguration(overrideConfig);
+ }
+}
diff --git a/core/tests/coretests/src/android/app/activity/ApplyOverrideConfigurationTest.java b/core/tests/coretests/src/android/app/activity/ApplyOverrideConfigurationTest.java
new file mode 100644
index 0000000..15ed77e
--- /dev/null
+++ b/core/tests/coretests/src/android/app/activity/ApplyOverrideConfigurationTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 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.app.activity;
+
+import android.app.UiAutomation;
+import android.content.res.Configuration;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.ActivityInstrumentationTestCase2;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ApplyOverrideConfigurationTest extends
+ ActivityInstrumentationTestCase2<ApplyOverrideConfigurationActivity> {
+
+ public static final int OVERRIDE_WIDTH = 9999;
+
+ public ApplyOverrideConfigurationTest() {
+ super(ApplyOverrideConfigurationActivity.class);
+ }
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ injectInstrumentation(InstrumentationRegistry.getInstrumentation());
+ getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
+ }
+
+ @Test
+ public void testConfigurationIsOverriden() throws Exception {
+ Configuration config = getActivity().getResources().getConfiguration();
+ assertEquals(OVERRIDE_WIDTH, config.smallestScreenWidthDp);
+
+ getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_90);
+
+ config = getActivity().getResources().getConfiguration();
+ assertEquals(OVERRIDE_WIDTH, config.smallestScreenWidthDp);
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception {
+ getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
+ super.tearDown();
+ }
+}
diff --git a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
index 3cadbf6..d4bb0f3 100644
--- a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
+++ b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
@@ -18,7 +18,7 @@
import android.annotation.NonNull;
import android.app.ResourcesManager;
import android.os.Binder;
-import android.test.suitebuilder.annotation.SmallTest;
+import android.support.test.filters.SmallTest;
import android.util.DisplayMetrics;
import android.util.LocaleList;
import android.util.TypedValue;
@@ -58,7 +58,7 @@
}
@Override
- protected DisplayMetrics getDisplayMetricsLocked(int displayId) {
+ protected DisplayMetrics getDisplayMetrics(int displayId) {
return mDisplayMetrics;
}
};
@@ -173,25 +173,12 @@
// The implementations should be the same.
assertSame(resources1.getImpl(), resources2.getImpl());
-
- final Configuration overrideConfig = new Configuration();
- overrideConfig.orientation = Configuration.ORIENTATION_LANDSCAPE;
- Resources resources3 = mResourcesManager.getResources(
- activity2, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY,
- overrideConfig, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
-
- // Since we requested new resources for activity2, the resource should be the same
- // as the one returned before for activity2.
- assertSame(resources2, resources3);
-
- // But the implementation has changed.
- assertNotSame(resources1.getImpl(), resources2.getImpl());
}
@SmallTest
public void testThemesGetUpdatedWithNewImpl() {
Binder activity1 = new Binder();
- Resources resources1 = mResourcesManager.getResources(
+ Resources resources1 = mResourcesManager.createBaseActivityResources(
activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, null,
CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
assertNotNull(resources1);
@@ -207,16 +194,59 @@
final Configuration overrideConfig = new Configuration();
overrideConfig.orientation = Configuration.ORIENTATION_LANDSCAPE;
- Resources resources2 = mResourcesManager.getResources(
- activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY,
- overrideConfig, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
- assertNotNull(resources2);
- assertSame(resources1, resources2);
- assertSame(resources2, theme.getResources());
+ mResourcesManager.updateResourcesForActivity(activity1, overrideConfig);
+ assertSame(resources1, theme.getResources());
// Make sure we can still access the data.
assertTrue(theme.resolveAttribute(android.R.attr.windowNoTitle, value, true));
assertEquals(TypedValue.TYPE_INT_BOOLEAN, value.type);
assertTrue(value.data != 0);
}
+
+ @SmallTest
+ public void testMultipleResourcesForOneActivityGetUpdatedWhenActivityBaseUpdates() {
+ Binder activity1 = new Binder();
+
+ // Create a Resources for the Activity.
+ Configuration config1 = new Configuration();
+ config1.densityDpi = 280;
+ Resources resources1 = mResourcesManager.createBaseActivityResources(
+ activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, config1,
+ CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+ assertNotNull(resources1);
+
+ // Create a Resources based on the Activity.
+ Configuration config2 = new Configuration();
+ config2.screenLayout |= Configuration.SCREENLAYOUT_ROUND_YES;
+ Resources resources2 = mResourcesManager.getResources(
+ activity1, APP_ONE_RES_DIR, null, null, null, Display.DEFAULT_DISPLAY, config2,
+ CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
+ assertNotNull(resources2);
+
+ assertNotSame(resources1, resources2);
+ assertNotSame(resources1.getImpl(), resources2.getImpl());
+
+ final Configuration expectedConfig1 = new Configuration();
+ expectedConfig1.setLocales(LocaleList.getAdjustedDefault());
+ expectedConfig1.densityDpi = 280;
+ assertEquals(expectedConfig1, resources1.getConfiguration());
+
+ // resources2 should be based on the Activity's override config, so the density should
+ // be the same as resources1.
+ final Configuration expectedConfig2 = new Configuration();
+ expectedConfig2.setLocales(LocaleList.getAdjustedDefault());
+ expectedConfig2.densityDpi = 280;
+ expectedConfig2.screenLayout |= Configuration.SCREENLAYOUT_ROUND_YES;
+ assertEquals(expectedConfig2, resources2.getConfiguration());
+
+ // Now update the Activity base override, and both resources should update.
+ config1.orientation = Configuration.ORIENTATION_LANDSCAPE;
+ mResourcesManager.updateResourcesForActivity(activity1, config1);
+
+ expectedConfig1.orientation = Configuration.ORIENTATION_LANDSCAPE;
+ assertEquals(expectedConfig1, resources1.getConfiguration());
+
+ expectedConfig2.orientation = Configuration.ORIENTATION_LANDSCAPE;
+ assertEquals(expectedConfig2, resources2.getConfiguration());
+ }
}
diff --git a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
index ec8cd71..43a61e3 100644
--- a/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
+++ b/core/tests/coretests/src/android/print/IPrintManagerParametersTest.java
@@ -31,6 +31,7 @@
import android.print.PrintAttributes.MediaSize;
import android.print.PrintAttributes.Resolution;
import android.printservice.PrintServiceInfo;
+import android.printservice.recommendation.IRecommendationsChangeListener;
import android.print.mockservice.MockPrintService;
import android.print.mockservice.PrintServiceCallbacks;
@@ -181,6 +182,17 @@
new Handler(Looper.getMainLooper()));
}
+ /**
+ * Create a IPrintServiceRecommendationsChangeListener object.
+ *
+ * @return the object
+ * @throws Exception if the object could not be created.
+ */
+ private IRecommendationsChangeListener
+ createMockIPrintServiceRecommendationsChangeListener() throws Exception {
+ return new PrintManager.PrintServiceRecommendationsChangeListenerWrapper(null,
+ new Handler(Looper.getMainLooper()));
+ }
/**
* Create a IPrinterDiscoveryObserver object.
@@ -559,6 +571,61 @@
}
/**
+ * test IPrintManager.addPrintServiceRecommendationsChangeListener
+ */
+ @MediumTest
+ public void testAddPrintServiceRecommendationsChangeListener() throws Exception {
+ final IRecommendationsChangeListener listener =
+ createMockIPrintServiceRecommendationsChangeListener();
+
+ mIPrintManager.addPrintServiceRecommendationsChangeListener(listener, mUserId);
+
+ assertException(new Invokable() {
+ @Override
+ public void run() throws Exception {
+ mIPrintManager.addPrintServiceRecommendationsChangeListener(null, mUserId);
+ }
+ }, NullPointerException.class);
+
+ // Cannot test bad user Id as these tests are allowed to call across users
+ }
+
+ /**
+ * test IPrintManager.removePrintServicesChangeListener
+ */
+ @MediumTest
+ public void testRemovePrintServiceRecommendationsChangeListener() throws Exception {
+ final IRecommendationsChangeListener listener =
+ createMockIPrintServiceRecommendationsChangeListener();
+
+ mIPrintManager.addPrintServiceRecommendationsChangeListener(listener, mUserId);
+ mIPrintManager.removePrintServiceRecommendationsChangeListener(listener, mUserId);
+
+ // Removing unknown listeners is a no-op
+ mIPrintManager.removePrintServiceRecommendationsChangeListener(listener, mUserId);
+
+ mIPrintManager.addPrintServiceRecommendationsChangeListener(listener, mUserId);
+ assertException(new Invokable() {
+ @Override
+ public void run() throws Exception {
+ mIPrintManager.removePrintServiceRecommendationsChangeListener(null, mUserId);
+ }
+ }, NullPointerException.class);
+
+ // Cannot test bad user Id as these tests are allowed to call across users
+ }
+
+ /**
+ * test IPrintManager.getPrintServiceRecommendations
+ */
+ @MediumTest
+ public void testGetPrintServiceRecommendations() throws Exception {
+ mIPrintManager.getPrintServiceRecommendations(mUserId);
+
+ // Cannot test bad user Id as these tests are allowed to call across users
+ }
+
+ /**
* test IPrintManager.createPrinterDiscoverySession
*/
@MediumTest
diff --git a/core/tests/coretests/src/android/view/ViewCaptureTest.java b/core/tests/coretests/src/android/view/ViewCaptureTest.java
new file mode 100644
index 0000000..15cfe23
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewCaptureTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2016 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.view;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.SparseIntArray;
+
+import com.android.frameworks.coretests.R;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(AndroidJUnit4.class)
+public class ViewCaptureTest {
+
+ private static final SparseIntArray EXPECTED_CHILDREN_VISIBILITY = new SparseIntArray();
+ static {
+ EXPECTED_CHILDREN_VISIBILITY.append(R.id.child1, View.VISIBLE);
+ EXPECTED_CHILDREN_VISIBILITY.append(R.id.child2, View.INVISIBLE);
+ EXPECTED_CHILDREN_VISIBILITY.append(R.id.child3, View.GONE);
+ EXPECTED_CHILDREN_VISIBILITY.append(R.id.child4, View.VISIBLE);
+ }
+
+ @Rule
+ public ActivityTestRule<ViewCaptureTestActivity> mActivityRule = new ActivityTestRule<>(
+ ViewCaptureTestActivity.class);
+
+ private Activity mActivity;
+ private ViewGroup mViewToCapture;
+
+ @Before
+ public void setUp() throws Exception {
+ mActivity = mActivityRule.getActivity();
+ mViewToCapture = (ViewGroup) mActivity.findViewById(R.id.capture);
+ }
+
+ @Test
+ @SmallTest
+ public void testCreateSnapshot() {
+ assertChildrenVisibility();
+ testCreateSnapshot(true, R.drawable.view_capture_test_no_children_golden);
+ assertChildrenVisibility();
+ testCreateSnapshot(false, R.drawable.view_capture_test_with_children_golden);
+ assertChildrenVisibility();
+ }
+
+ private void testCreateSnapshot(boolean skipChildren, int goldenResId) {
+ Bitmap result = mViewToCapture.createSnapshot(Bitmap.Config.ARGB_8888, 0, skipChildren);
+ Bitmap golden = BitmapFactory.decodeResource(mActivity.getResources(), goldenResId);
+ assertTrue(golden.sameAs(result));
+ }
+
+ private void assertChildrenVisibility() {
+ for (int i = 0; i < EXPECTED_CHILDREN_VISIBILITY.size(); i++) {
+ int id = EXPECTED_CHILDREN_VISIBILITY.keyAt(i);
+ View child = mViewToCapture.findViewById(id);
+ Assert.assertEquals(EXPECTED_CHILDREN_VISIBILITY.get(id), child.getVisibility());
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/view/ViewCaptureTestActivity.java b/core/tests/coretests/src/android/view/ViewCaptureTestActivity.java
new file mode 100644
index 0000000..20e3eb5
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ViewCaptureTestActivity.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2016 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.view;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.Bundle;
+import com.android.frameworks.coretests.R;
+
+public class ViewCaptureTestActivity extends Activity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.view_capture_snapshot);
+ }
+}
diff --git a/docs/docs-redirect-index.html b/docs/docs-redirect-index.html
index dc0bc72..ea5f186 100644
--- a/docs/docs-redirect-index.html
+++ b/docs/docs-redirect-index.html
@@ -1,8 +1,8 @@
-<html>
-<head>
-<meta http-equiv="refresh" content="0;url=framework/index.html">
-</head>
-<body>
-<a href="framework/index.html">click here if you are not redirected</a>
-</body>
-</html>
+<html>
+<head>
+<meta http-equiv="refresh" content="0;url=framework/index.html">
+</head>
+<body>
+<a href="framework/index.html">click here if you are not redirected</a>
+</body>
+</html>
diff --git a/docs/docs-redirect.html b/docs/docs-redirect.html
index 4e0743e..d83564c 100644
--- a/docs/docs-redirect.html
+++ b/docs/docs-redirect.html
@@ -1,8 +1,8 @@
-<html>
-<head>
-<meta http-equiv="refresh" content="0;url=docs/offline.html">
-</head>
-<body>
-<a href="docs/offline.html">click here if you are not redirected</a>
-</body>
-</html>
+<html>
+<head>
+<meta http-equiv="refresh" content="0;url=docs/offline.html">
+</head>
+<body>
+<a href="docs/offline.html">click here if you are not redirected</a>
+</body>
+</html>
diff --git a/docs/docs-samples-redirect.html b/docs/docs-samples-redirect.html
index 4e549e6..abefe6c 100644
--- a/docs/docs-samples-redirect.html
+++ b/docs/docs-samples-redirect.html
@@ -1,8 +1,8 @@
-<html>
-<head>
-<meta http-equiv="refresh" content="0;url=../../samples/">
-</head>
-<body>
-<a href="../../samples/">click here if you are not redirected</a>
-</body>
-</html>
+<html>
+<head>
+<meta http-equiv="refresh" content="0;url=../../samples/">
+</head>
+<body>
+<a href="../../samples/">click here if you are not redirected</a>
+</body>
+</html>
diff --git a/docs/html/guide/topics/resources/localization.jd b/docs/html/guide/topics/resources/localization.jd
index 0a96a15..e60ca9f 100644
--- a/docs/html/guide/topics/resources/localization.jd
+++ b/docs/html/guide/topics/resources/localization.jd
@@ -1,484 +1,484 @@
-page.title=Localizing with Resources
-parent.title=Application Resources
-page.tags="localizing","localization","resources", "formats", "l10n"
-parent.link=index.html
-@jd:body
-
-<div id="qv-wrapper">
- <div id="qv">
-
-<h2>Quickview</h2>
-
-<ul>
- <li>Use resource sets to create a localized app.</li>
- <li>Android loads the correct resource set for the user's language and locale.</li>
- <li>If localized resources are not available, Android loads your default resources.</li>
-</ul>
-
-<h2>In this document</h2>
-<ol>
- <li><a href="#resource-switching">Overview: Resource-Switching in Android</a></li>
-<li><a href="#using-framework">Using Resources for Localization</a></li>
-<li><a href="#strategies">Localization Tips</a></li>
-<li><a href="#testing">Testing Localized Applications</a></li>
-</ol>
-
-<h2>See also</h2>
- <ol>
- <li><a href="{@docRoot}distribute/tools/localization-checklist.html">Localization Checklist</a></li>
- <li><a href="{@docRoot}guide/topics/resources/providing-resources.html">Providing Resources</a></li>
- <li><a href="{@docRoot}guide/topics/ui/declaring-layout.html">Layouts</a></li>
- <li><a href="{@docRoot}reference/android/app/Activity.html#ActivityLifecycle">Activity Lifecycle</a></li>
-</ol>
-</div>
-</div>
-
-<p>Android will run on many devices in many regions. To reach the most users,
-your application should handle text, audio files, numbers, currency, and
-graphics in ways appropriate to the locales where your application will be used.
-</p>
-
-<p>This document describes best practices for localizing Android
-applications. The principles apply whether you are developing your application
-using ADT with Eclipse, Ant-based tools, or any other IDE. </p>
-
-<p>You should already have a working knowledge of Java and be familiar with
-Android resource loading, the declaration of user interface elements in XML,
-development considerations such as Activity lifecycle, and general principles of
-internationalization and localization. </p>
-
-<p>It is good practice to use the Android resource framework to separate the
-localized aspects of your application as much as possible from the core Java
-functionality:</p>
-
-<ul>
- <li>You can put most or all of the <em>contents</em> of your application's
-user interface into resource files, as described in this document and in <a
-href="{@docRoot}guide/topics/resources/providing-resources.html">Providing Resources</a>.</li>
- <li>The <em>behavior</em> of the user interface, on the other hand, is driven
-by your Java code.
- For example, if users input data that needs to be formatted or sorted
-differently depending on locale, then you would use Java to handle the data
-programmatically. This document does not cover how to localize your Java code.
-</li>
-</ul>
-
-<p>For a short guide to localizing strings in your app, see the training lesson, <a
-href="{@docRoot}training/basics/supporting-devices/languages.html">Supporting Different Languages</a>. </p>
-
-
-<h2 id="resource-switching">Overview: Resource-Switching in Android</h2>
-
-<p>Resources are text strings, layouts, sounds, graphics, and any other static
-data that your Android application needs. An application can include multiple
-sets of resources, each customized for a different device configuration. When a
-user runs the application, Android automatically selects and loads the
-resources that best match the device.</p>
-
-<p>(This document focuses on localization and locale. For a complete description
-of resource-switching and all the types of configurations that you can
-specify — screen orientation, touchscreen type, and so on — see <a
-href="{@docRoot}guide/topics/resources/providing-resources.html#AlternativeResources">Providing
-Alternative Resources</a>.)</p>
-
-<table border="0" cellspacing="0" cellpadding="0">
- <tr border="0">
- <td width="180" style="border: 0pt none ;"><p class="special-note">
- <strong>When you write your application:</strong>
- <br><br>
- You create a set of default resources, plus alternatives to be used in
- different locales.</p></td>
- <td style="border: 0pt none; padding:0">
- <p style="border:0; padding:0"><img src="../../../images/resources/right-arrow.png" alt="right-arrow"
- width="51" height="17"></p></td>
- <td width="180" style="border: 0pt none ;"><p class="special-note">
- <strong>When a user runs your application:</strong>
- <br><br>The Android system selects which resources to load, based on the
- device's locale.</p></td>
- </tr>
-</table>
-
-<p>When you write your application, you create default and alternative resources
-for your application to use. To create resources, you place files within
-specially named subdirectories of the project's <code>res/</code> directory.
-</p>
-
-
-
-<h3 id="defaults-r-important">Why Default Resources Are Important</h3>
-
-<p>Whenever the application runs in a locale for which you have not provided
-locale-specific text, Android will load the default strings from
-<code>res/values/strings.xml</code>. If this default file is absent, or if it
-is missing a string that your application needs, then your application will not run
-and will show an error.
-The example below illustrates what can happen when the default text file is incomplete. </p>
-
-<p><em>Example:</em>
-<p>An application's Java code refers to just two strings, <code>text_a</code> and
- <code>text_b</code>. This application includes a localized resource file
- (<code>res/values-en/strings.xml</code>) that defines <code>text_a</code> and
- <code>text_b</code> in English. This application also includes a default
- resource file (<code>res/values/strings.xml</code>) that includes a
-definition for <code>text_a</code>, but not for <code>text_b</code>:
-<ul>
- <li>This application might compile without a problem. An IDE such as Eclipse
- will not highlight any errors if a resource is missing.</li>
- <li>When this application is launched on a device with locale set to English,
- the application might run without a problem, because
- <code>res/values-en/strings.xml</code> contains both of the needed text
- strings.</li>
- <li>However, <strong>the user will see an error message and a Force Close
- button</strong> when this application is launched on a device set to a
- language other than English. The application will not load.</li>
-</ul>
-
-
-<p>To prevent this situation, make sure that a <code>res/values/strings.xml</code>
- file exists and that it defines every needed string. The situation applies to
- all types of resources, not just strings: You
- need to create a set of default resource files containing all
- the resources that your application calls upon — layouts, drawables,
- animations, etc. For information about testing, see <a href="#test-for-default">
- Testing for Default Resources</a>.</p>
-
-<h2 id="using-framework">Using Resources for Localization</h2>
-
-<h3 id="creating-defaults">How to Create Default Resources</h3>
-
-<p>Put the application's default text in
-a file with the following location and name:</p>
-<p><code> res/values/strings.xml</code> (required directory)</p>
-
-<p>The text strings in <code>res/values/strings.xml</code> should use the
-default language, which is the language that you expect most of your application's users to
-speak. </p>
-
-<p>The default resource set must also include any default drawables and layouts,
- and can include other types of resources such as animations.
-<br>
- <code> res/drawable/</code>(required directory holding at least
- one graphic file, for the application's icon on Google Play)<br>
- <code> res/layout/</code> (required directory holding an XML
- file that defines the default layout)<br>
- <code> res/anim/</code> (required if you have any
- <code>res/anim-<em><qualifiers></em></code> folders)<br>
- <code> res/xml/</code> (required if you have any
- <code>res/xml-<em><qualifiers></em></code> folders)<br>
- <code> res/raw/</code> (required if you have any
- <code>res/raw-<em><qualifiers></em></code> folders)
-</p>
-
-<p class="note"><strong>Tip:</strong> In your code, examine each reference to
- an Android resource. Make sure that a default resource is defined for each
- one. Also make sure that the default string file is complete: A <em>
- localized</em> string file can contain a subset of the strings, but the
- <em>default</em> string file must contain them all.
-</p>
-
-<h3 id="creating-alternatives">How to Create Alternative Resources</h3>
-
-<p>A large part of localizing an application is providing alternative text for
-different languages. In some cases you will also provide alternative graphics,
-sounds, layouts, and other locale-specific resources. </p>
-
-<p>An application can specify many <code>res/<em><qualifiers></em>/</code>
-directories, each with different qualifiers. To create an alternative resource for
-a different locale, you use a qualifier that specifies a language or a
-language-region combination. (The name of a resource directory must conform
-to the naming scheme described in
-<a href="{@docRoot}guide/topics/resources/providing-resources.html#AlternativeResources">Providing
-Alternative Resources</a>,
-or else it will not compile.)</p>
-
-<p><em>Example:</em></p>
-
-<p>Suppose that your application's default language is English. Suppose also
-that you want to localize all the text in your application to French, and most
-of the text in your application (everything except the application's title) to
-Japanese. In this case, you could create three alternative <code>strings.xml</code>
-files, each stored in a locale-specific resource directory:</p>
-
-<ol>
- <li><code>res/values/strings.xml</code><br>
- Contains English text for all the strings that the application uses,
-including text for a string named <code>title</code>.</li>
- <li><code>res/values-fr/strings.xml</code><br>
- Contain French text for all the strings, including <code>title</code>.</li>
- <li><code>res/values-ja/strings.xml</code><br>
- Contain Japanese text for all the strings <em>except</em>
-<code>title</code>.<br>
- <code></code></li>
-</ol>
-
-<p>If your Java code refers to <code>R.string.title</code>, here is what will
-happen at runtime:</p>
-
-<ul>
- <li>If the device is set to any language other than French, Android will load
-<code>title</code> from the <code>res/values/strings.xml</code> file.</li>
- <li>If the device is set to French, Android will load <code>title</code> from
-the <code>res/values-fr/strings.xml</code> file.</li>
-</ul>
-
-<p>Notice that if the device is set to Japanese, Android will look for
-<code>title</code> in the <code>res/values-ja/strings.xml</code> file. But
-because no such string is included in that file, Android will fall back to the
-default, and will load <code>title</code> in English from the
-<code>res/values/strings.xml</code> file. </p>
-
-<h3 id="resource-precedence">Which Resources Take Precedence?</h3>
-
-<p> If multiple resource files match a device's configuration, Android follows a
-set of rules in deciding which file to use. Among the qualifiers that can be
-specified in a resource directory name, <strong>locale almost always takes
-precedence</strong>. </p>
-<p><em>Example:</em></p>
-
-<p>Assume that an application includes a default set of graphics and two other
-sets of graphics, each optimized for a different device setup:</p>
-
-<ul>
- <li><code>res/drawable/</code><br>
- Contains
- default graphics.</li>
- <li><code>res/drawable-small-land-stylus/</code><br>
- Contains graphics optimized for use with a device that expects input from a
- stylus and has a QVGA low-density screen in landscape orientation.</li>
- <li><code>res/drawable-ja/</code> <br>
- Contains graphics optimized for use with Japanese.</li>
-</ul>
-
-<p>If the application runs on a device that is configured to use Japanese,
-Android will load graphics from <code>res/drawable-ja/</code>, even if the
-device happens to be one that expects input from a stylus and has a QVGA
-low-density screen in landscape orientation.</p>
-
-<p class="note"><strong>Exception:</strong> The only qualifiers that take
-precedence over locale in the selection process are MCC and MNC (mobile country
-code and mobile network code). </p>
-
-<p><em>Example:</em></p>
-
-<p>Assume that you have the following situation:</p>
-
-<ul>
- <li>The application code calls for <code>R.string.text_a</code></li>
- <li>Two relevant resource files are available:
- <ul>
- <li><code>res/values-mcc404/strings.xml</code>, which includes
-<code>text_a</code> in the application's default language, in this case
-English.</li>
- <li><code>res/values-hi/strings.xml</code>, which includes
-<code>text_a</code> in Hindi.</li>
- </ul>
- </li>
- <li>The application is running on a device that has the following
-configuration:
- <ul>
- <li>The SIM card is connected to a mobile network in India (MCC 404).</li>
- <li>The language is set to Hindi (<code>hi</code>).</li>
- </ul>
- </li>
-</ul>
-
-<p>Android will load <code>text_a</code> from
-<code>res/values-mcc404/strings.xml</code> (in English), even if the device is
-configured for Hindi. That is because in the resource-selection process, Android
-will prefer an MCC match over a language match. </p>
-
-<p>The selection process is not always as straightforward as these examples
-suggest. Please read <a
-href="{@docRoot}guide/topics/resources/providing-resources.html#BestMatch">How Android Finds
-the Best-matching Resource</a> for a more nuanced description of the
-process. All the qualifiers are described and listed in order of
-precedence in <a
-href="{@docRoot}guide/topics/resources/providing-resources.html#table2">Table 2 of Providing
-Alternative Resources</a>.</p>
-
-<h3 id="referring-to-resources">Referring to Resources in Java</h3>
-
-<p>In your application's Java code, you refer to resources using the syntax
-<code>R.<em>resource_type</em>.<em>resource_name</em></code> or
-<code>android.R.<em>resource_type</em>.<em>resource_name</em></code><em>.</em>
-For more about this, see <a
-href="{@docRoot}guide/topics/resources/accessing-resources.html">Accessing Resources</a>.</p>
-
-<h2 id="checklist">Localization Checklist</h2>
-
-<p>For a complete overview of the process of localizing and distributing an Android application,
-see the <a href="{@docRoot}distribute/tools/localization-checklist.html">Localization
-Checklist</a> document.</p>
-
-<h2 id="strategies">Localization Tips</h2>
-
-<h4 id="failing2">Design your application to work in any locale</h4>
-
-<p>You cannot assume anything about the device on which a user will
-run your application. The device might have hardware that you were not
-anticipating, or it might be set to a locale that you did not plan for or that
-you cannot test. Design your application so that it will function normally or fail gracefully no
-matter what device it runs on.</p>
-
-<p class="note"><strong>Important:</strong> Make sure that your application
-includes a full set of default resources.</p> <p>Make sure to include
-<code>res/drawable/</code> and a <code>res/values/</code> folders (without any
-additional modifiers in the folder names) that contain all the images and text
-that your application will need. </p>
-
-<p>If an application is missing even one default resource, it will not run on a
- device that is set to an unsupported locale. For example, the
- <code>res/values/strings.xml</code> default file might lack one string that
- the application needs: When the application runs in an unsupported locale and
- attempts to load <code>res/values/strings.xml</code>, the user will see an
- error message and a Force Close button. An IDE such as Eclipse will not
- highlight this kind of error, and you will not see the problem when you
- test the application on a device or emulator that is set to a supported locale.</p>
-
-<p>For more information, see <a href="#test-for-default">Testing for Default Resources</a>.</p>
-
-<h4>Design a flexible layout</h4>
-
-<p> If you need to rearrange your layout to fit a certain language (for example
-German with its long words), you can create an alternative layout for that
-language (for example <code>res/layout-de/main.xml</code>). However, doing this
-can make your application harder to maintain. It is better to create a single
-layout that is more flexible.</p>
-
-<p>Another typical situation is a language that requires something different in
-its layout. For example, you might have a contact form that should include two
-name fields when the application runs in Japanese, but three name fields when
-the application runs in some other language. You could handle this in either of
-two ways:</p>
-
-<ul>
- <li>Create one layout with a field that you can programmatically enable or
-disable, based on the language, or</li>
- <li>Have the main layout include another layout that includes the changeable
-field. The second layout can have different configurations for different
-languages.</li>
-</ul>
-
-<h4>Avoid creating more resource files and text strings than you need</h4>
-
-<p>You probably do not need to create a locale-specific
-alternative for every resource in your application. For example, the layout
-defined in the <code>res/layout/main.xml</code> file might work in any locale,
-in which case there would be no need to create any alternative layout files.
-</p>
-
-<p>Also, you might not need to create alternative text for every
-string. For example, assume the following:</p>
-
-<ul>
- <li>Your application's default language is American
-English. Every string that the application uses is defined, using American
-English spellings, in <code>res/values/strings.xml</code>. </li>
-
- <li>For a few important phrases, you want to provide
-British English spelling. You want these alternative strings to be used when your
-application runs on a device in the United Kingdom. </li>
-</ul>
-
-<p>To do this, you could create a small file called
-<code>res/values-en-rGB/strings.xml</code> that includes only the strings that
-should be different when the application runs in the U.K. For all the rest of
-the strings, the application will fall back to the defaults and use what is
-defined in <code>res/values/strings.xml</code>.</p>
-
-<h4>Use the Android Context object for manual locale lookup</h4>
-
-<p>You can look up the locale using the {@link android.content.Context} object
-that Android makes available:</p>
-
-<pre>String locale = context.getResources().getConfiguration().locale.getDisplayName();</pre>
-
-<h2 id="testing">Testing Localized Applications</h2>
-
-<h3 id="device">Testing on a Device</h3>
-<p>Keep in mind that the device you are testing may be significantly different from
- the devices available to consumers in other geographies. The locales available
- on your device may differ from those available on other devices. Also, the
- resolution and density of the device screen may differ, which could affect
- the display of strings and drawables in your UI.</p>
-
-<p>To change the locale or language on a device, use the Settings application.</p>
-
-<h3 id="emulator">Testing on an Emulator</h3>
-
-<p>For details about using the emulator, see See <a
-href="{@docRoot}tools/help/emulator.html">Android Emulator</a>.</p>
-<h4>Creating and using a custom locale</h4>
-
-<p>A "custom" locale is a language/region combination that the Android
-system image does not explicitly support. (For a list of supported locales in
-Android platforms see the Version Notes in the <a
-href="{@docRoot}sdk/index.html">SDK</a> tab). You can test
-how your application will run in a custom locale by creating a custom locale in
-the emulator. There are two ways to do this:</p>
-
-<ul>
- <li>Use the Custom Locale application, which is accessible from the
-Application tab. (After you create a custom locale, switch to it by
-pressing and holding the locale name.)</li>
- <li>Change to a custom locale from the adb shell, as described below.</li>
-</ul>
-
-<p>When you set the emulator to a locale that is not available in the Android
-system image, the system itself will display in its default language. Your
-application, however, should localize properly.</p>
-
-<h4>Changing the emulator locale from the adb shell</h4>
-
-<p>To change the locale in the emulator by using the adb shell. </p>
-
-<ol>
- <li>Pick the locale you want to test and determine its BCP-47 language tag, for
-example, Canadian French would be <code>fr-CA</code>.<br>
- </li>
- <li>Launch an emulator.</li>
- <li>From a command-line shell on the host computer, run the following
-command:<br>
- <code>adb shell</code><br>
- or if you have a device attached, specify that you want the emulator by adding
-the <code>-e</code> option:<br>
- <code>adb -e shell</code></li>
- <li>At the adb shell prompt (<code>#</code>), run this command: <br>
- <code>setprop persist.sys.locale [<em>BCP-47 language tag</em>];stop;sleep 5;start <br>
- </code>Replace bracketed sections with the appropriate codes from Step
-1.</li>
-</ol>
-
-<p>For instance, to test in Canadian French:</p>
-
-<p><code>setprop persist.sys.locale fr-CA;stop;sleep 5;start </code></p>
-
-<p>This will cause the emulator to restart. (It will look like a full reboot,
-but it is not.) Once the Home screen appears again, re-launch your application (for
-example, click the Run icon in Eclipse), and the application will launch with
-the new locale. </p>
-
-<h3 id="test-for-default">Testing for Default Resources</h3>
-<p>Here's how to test whether an application includes every string resource that it needs: </p>
-<ol><li>Set the emulator or device to a language that your application does not
- support. For example, if the application has French strings in
- <code>res/values-fr/</code> but does not have any Spanish strings in
- <code>res/values-es/</code>, then set the emulator's locale to Spanish.
- (You can use the Custom Locale application to set the emulator to an
- unsupported locale.)</li>
- <li>Run the application.</li>
-<li>If the application shows an error message and a Force Close button, it might
- be looking for a string that is not available. Make sure that your
- <code>res/values/strings.xml</code> file includes a definition for
- every string that the application uses.</li>
-</ol>
-</p>
-
-<p>If the test is successful, repeat it for other types of
- configurations. For example, if the application has a layout file called
- <code>res/layout-land/main.xml</code> but does not contain a file called
- <code>res/layout-port/main.xml</code>, then set the emulator or device to
- portrait orientation and see if the application will run.
-
-
-
+page.title=Localizing with Resources
+parent.title=Application Resources
+page.tags="localizing","localization","resources", "formats", "l10n"
+parent.link=index.html
+@jd:body
+
+<div id="qv-wrapper">
+ <div id="qv">
+
+<h2>Quickview</h2>
+
+<ul>
+ <li>Use resource sets to create a localized app.</li>
+ <li>Android loads the correct resource set for the user's language and locale.</li>
+ <li>If localized resources are not available, Android loads your default resources.</li>
+</ul>
+
+<h2>In this document</h2>
+<ol>
+ <li><a href="#resource-switching">Overview: Resource-Switching in Android</a></li>
+<li><a href="#using-framework">Using Resources for Localization</a></li>
+<li><a href="#strategies">Localization Tips</a></li>
+<li><a href="#testing">Testing Localized Applications</a></li>
+</ol>
+
+<h2>See also</h2>
+ <ol>
+ <li><a href="{@docRoot}distribute/tools/localization-checklist.html">Localization Checklist</a></li>
+ <li><a href="{@docRoot}guide/topics/resources/providing-resources.html">Providing Resources</a></li>
+ <li><a href="{@docRoot}guide/topics/ui/declaring-layout.html">Layouts</a></li>
+ <li><a href="{@docRoot}reference/android/app/Activity.html#ActivityLifecycle">Activity Lifecycle</a></li>
+</ol>
+</div>
+</div>
+
+<p>Android will run on many devices in many regions. To reach the most users,
+your application should handle text, audio files, numbers, currency, and
+graphics in ways appropriate to the locales where your application will be used.
+</p>
+
+<p>This document describes best practices for localizing Android
+applications. The principles apply whether you are developing your application
+using ADT with Eclipse, Ant-based tools, or any other IDE. </p>
+
+<p>You should already have a working knowledge of Java and be familiar with
+Android resource loading, the declaration of user interface elements in XML,
+development considerations such as Activity lifecycle, and general principles of
+internationalization and localization. </p>
+
+<p>It is good practice to use the Android resource framework to separate the
+localized aspects of your application as much as possible from the core Java
+functionality:</p>
+
+<ul>
+ <li>You can put most or all of the <em>contents</em> of your application's
+user interface into resource files, as described in this document and in <a
+href="{@docRoot}guide/topics/resources/providing-resources.html">Providing Resources</a>.</li>
+ <li>The <em>behavior</em> of the user interface, on the other hand, is driven
+by your Java code.
+ For example, if users input data that needs to be formatted or sorted
+differently depending on locale, then you would use Java to handle the data
+programmatically. This document does not cover how to localize your Java code.
+</li>
+</ul>
+
+<p>For a short guide to localizing strings in your app, see the training lesson, <a
+href="{@docRoot}training/basics/supporting-devices/languages.html">Supporting Different Languages</a>. </p>
+
+
+<h2 id="resource-switching">Overview: Resource-Switching in Android</h2>
+
+<p>Resources are text strings, layouts, sounds, graphics, and any other static
+data that your Android application needs. An application can include multiple
+sets of resources, each customized for a different device configuration. When a
+user runs the application, Android automatically selects and loads the
+resources that best match the device.</p>
+
+<p>(This document focuses on localization and locale. For a complete description
+of resource-switching and all the types of configurations that you can
+specify — screen orientation, touchscreen type, and so on — see <a
+href="{@docRoot}guide/topics/resources/providing-resources.html#AlternativeResources">Providing
+Alternative Resources</a>.)</p>
+
+<table border="0" cellspacing="0" cellpadding="0">
+ <tr border="0">
+ <td width="180" style="border: 0pt none ;"><p class="special-note">
+ <strong>When you write your application:</strong>
+ <br><br>
+ You create a set of default resources, plus alternatives to be used in
+ different locales.</p></td>
+ <td style="border: 0pt none; padding:0">
+ <p style="border:0; padding:0"><img src="../../../images/resources/right-arrow.png" alt="right-arrow"
+ width="51" height="17"></p></td>
+ <td width="180" style="border: 0pt none ;"><p class="special-note">
+ <strong>When a user runs your application:</strong>
+ <br><br>The Android system selects which resources to load, based on the
+ device's locale.</p></td>
+ </tr>
+</table>
+
+<p>When you write your application, you create default and alternative resources
+for your application to use. To create resources, you place files within
+specially named subdirectories of the project's <code>res/</code> directory.
+</p>
+
+
+
+<h3 id="defaults-r-important">Why Default Resources Are Important</h3>
+
+<p>Whenever the application runs in a locale for which you have not provided
+locale-specific text, Android will load the default strings from
+<code>res/values/strings.xml</code>. If this default file is absent, or if it
+is missing a string that your application needs, then your application will not run
+and will show an error.
+The example below illustrates what can happen when the default text file is incomplete. </p>
+
+<p><em>Example:</em>
+<p>An application's Java code refers to just two strings, <code>text_a</code> and
+ <code>text_b</code>. This application includes a localized resource file
+ (<code>res/values-en/strings.xml</code>) that defines <code>text_a</code> and
+ <code>text_b</code> in English. This application also includes a default
+ resource file (<code>res/values/strings.xml</code>) that includes a
+definition for <code>text_a</code>, but not for <code>text_b</code>:
+<ul>
+ <li>This application might compile without a problem. An IDE such as Eclipse
+ will not highlight any errors if a resource is missing.</li>
+ <li>When this application is launched on a device with locale set to English,
+ the application might run without a problem, because
+ <code>res/values-en/strings.xml</code> contains both of the needed text
+ strings.</li>
+ <li>However, <strong>the user will see an error message and a Force Close
+ button</strong> when this application is launched on a device set to a
+ language other than English. The application will not load.</li>
+</ul>
+
+
+<p>To prevent this situation, make sure that a <code>res/values/strings.xml</code>
+ file exists and that it defines every needed string. The situation applies to
+ all types of resources, not just strings: You
+ need to create a set of default resource files containing all
+ the resources that your application calls upon — layouts, drawables,
+ animations, etc. For information about testing, see <a href="#test-for-default">
+ Testing for Default Resources</a>.</p>
+
+<h2 id="using-framework">Using Resources for Localization</h2>
+
+<h3 id="creating-defaults">How to Create Default Resources</h3>
+
+<p>Put the application's default text in
+a file with the following location and name:</p>
+<p><code> res/values/strings.xml</code> (required directory)</p>
+
+<p>The text strings in <code>res/values/strings.xml</code> should use the
+default language, which is the language that you expect most of your application's users to
+speak. </p>
+
+<p>The default resource set must also include any default drawables and layouts,
+ and can include other types of resources such as animations.
+<br>
+ <code> res/drawable/</code>(required directory holding at least
+ one graphic file, for the application's icon on Google Play)<br>
+ <code> res/layout/</code> (required directory holding an XML
+ file that defines the default layout)<br>
+ <code> res/anim/</code> (required if you have any
+ <code>res/anim-<em><qualifiers></em></code> folders)<br>
+ <code> res/xml/</code> (required if you have any
+ <code>res/xml-<em><qualifiers></em></code> folders)<br>
+ <code> res/raw/</code> (required if you have any
+ <code>res/raw-<em><qualifiers></em></code> folders)
+</p>
+
+<p class="note"><strong>Tip:</strong> In your code, examine each reference to
+ an Android resource. Make sure that a default resource is defined for each
+ one. Also make sure that the default string file is complete: A <em>
+ localized</em> string file can contain a subset of the strings, but the
+ <em>default</em> string file must contain them all.
+</p>
+
+<h3 id="creating-alternatives">How to Create Alternative Resources</h3>
+
+<p>A large part of localizing an application is providing alternative text for
+different languages. In some cases you will also provide alternative graphics,
+sounds, layouts, and other locale-specific resources. </p>
+
+<p>An application can specify many <code>res/<em><qualifiers></em>/</code>
+directories, each with different qualifiers. To create an alternative resource for
+a different locale, you use a qualifier that specifies a language or a
+language-region combination. (The name of a resource directory must conform
+to the naming scheme described in
+<a href="{@docRoot}guide/topics/resources/providing-resources.html#AlternativeResources">Providing
+Alternative Resources</a>,
+or else it will not compile.)</p>
+
+<p><em>Example:</em></p>
+
+<p>Suppose that your application's default language is English. Suppose also
+that you want to localize all the text in your application to French, and most
+of the text in your application (everything except the application's title) to
+Japanese. In this case, you could create three alternative <code>strings.xml</code>
+files, each stored in a locale-specific resource directory:</p>
+
+<ol>
+ <li><code>res/values/strings.xml</code><br>
+ Contains English text for all the strings that the application uses,
+including text for a string named <code>title</code>.</li>
+ <li><code>res/values-fr/strings.xml</code><br>
+ Contain French text for all the strings, including <code>title</code>.</li>
+ <li><code>res/values-ja/strings.xml</code><br>
+ Contain Japanese text for all the strings <em>except</em>
+<code>title</code>.<br>
+ <code></code></li>
+</ol>
+
+<p>If your Java code refers to <code>R.string.title</code>, here is what will
+happen at runtime:</p>
+
+<ul>
+ <li>If the device is set to any language other than French, Android will load
+<code>title</code> from the <code>res/values/strings.xml</code> file.</li>
+ <li>If the device is set to French, Android will load <code>title</code> from
+the <code>res/values-fr/strings.xml</code> file.</li>
+</ul>
+
+<p>Notice that if the device is set to Japanese, Android will look for
+<code>title</code> in the <code>res/values-ja/strings.xml</code> file. But
+because no such string is included in that file, Android will fall back to the
+default, and will load <code>title</code> in English from the
+<code>res/values/strings.xml</code> file. </p>
+
+<h3 id="resource-precedence">Which Resources Take Precedence?</h3>
+
+<p> If multiple resource files match a device's configuration, Android follows a
+set of rules in deciding which file to use. Among the qualifiers that can be
+specified in a resource directory name, <strong>locale almost always takes
+precedence</strong>. </p>
+<p><em>Example:</em></p>
+
+<p>Assume that an application includes a default set of graphics and two other
+sets of graphics, each optimized for a different device setup:</p>
+
+<ul>
+ <li><code>res/drawable/</code><br>
+ Contains
+ default graphics.</li>
+ <li><code>res/drawable-small-land-stylus/</code><br>
+ Contains graphics optimized for use with a device that expects input from a
+ stylus and has a QVGA low-density screen in landscape orientation.</li>
+ <li><code>res/drawable-ja/</code> <br>
+ Contains graphics optimized for use with Japanese.</li>
+</ul>
+
+<p>If the application runs on a device that is configured to use Japanese,
+Android will load graphics from <code>res/drawable-ja/</code>, even if the
+device happens to be one that expects input from a stylus and has a QVGA
+low-density screen in landscape orientation.</p>
+
+<p class="note"><strong>Exception:</strong> The only qualifiers that take
+precedence over locale in the selection process are MCC and MNC (mobile country
+code and mobile network code). </p>
+
+<p><em>Example:</em></p>
+
+<p>Assume that you have the following situation:</p>
+
+<ul>
+ <li>The application code calls for <code>R.string.text_a</code></li>
+ <li>Two relevant resource files are available:
+ <ul>
+ <li><code>res/values-mcc404/strings.xml</code>, which includes
+<code>text_a</code> in the application's default language, in this case
+English.</li>
+ <li><code>res/values-hi/strings.xml</code>, which includes
+<code>text_a</code> in Hindi.</li>
+ </ul>
+ </li>
+ <li>The application is running on a device that has the following
+configuration:
+ <ul>
+ <li>The SIM card is connected to a mobile network in India (MCC 404).</li>
+ <li>The language is set to Hindi (<code>hi</code>).</li>
+ </ul>
+ </li>
+</ul>
+
+<p>Android will load <code>text_a</code> from
+<code>res/values-mcc404/strings.xml</code> (in English), even if the device is
+configured for Hindi. That is because in the resource-selection process, Android
+will prefer an MCC match over a language match. </p>
+
+<p>The selection process is not always as straightforward as these examples
+suggest. Please read <a
+href="{@docRoot}guide/topics/resources/providing-resources.html#BestMatch">How Android Finds
+the Best-matching Resource</a> for a more nuanced description of the
+process. All the qualifiers are described and listed in order of
+precedence in <a
+href="{@docRoot}guide/topics/resources/providing-resources.html#table2">Table 2 of Providing
+Alternative Resources</a>.</p>
+
+<h3 id="referring-to-resources">Referring to Resources in Java</h3>
+
+<p>In your application's Java code, you refer to resources using the syntax
+<code>R.<em>resource_type</em>.<em>resource_name</em></code> or
+<code>android.R.<em>resource_type</em>.<em>resource_name</em></code><em>.</em>
+For more about this, see <a
+href="{@docRoot}guide/topics/resources/accessing-resources.html">Accessing Resources</a>.</p>
+
+<h2 id="checklist">Localization Checklist</h2>
+
+<p>For a complete overview of the process of localizing and distributing an Android application,
+see the <a href="{@docRoot}distribute/tools/localization-checklist.html">Localization
+Checklist</a> document.</p>
+
+<h2 id="strategies">Localization Tips</h2>
+
+<h4 id="failing2">Design your application to work in any locale</h4>
+
+<p>You cannot assume anything about the device on which a user will
+run your application. The device might have hardware that you were not
+anticipating, or it might be set to a locale that you did not plan for or that
+you cannot test. Design your application so that it will function normally or fail gracefully no
+matter what device it runs on.</p>
+
+<p class="note"><strong>Important:</strong> Make sure that your application
+includes a full set of default resources.</p> <p>Make sure to include
+<code>res/drawable/</code> and a <code>res/values/</code> folders (without any
+additional modifiers in the folder names) that contain all the images and text
+that your application will need. </p>
+
+<p>If an application is missing even one default resource, it will not run on a
+ device that is set to an unsupported locale. For example, the
+ <code>res/values/strings.xml</code> default file might lack one string that
+ the application needs: When the application runs in an unsupported locale and
+ attempts to load <code>res/values/strings.xml</code>, the user will see an
+ error message and a Force Close button. An IDE such as Eclipse will not
+ highlight this kind of error, and you will not see the problem when you
+ test the application on a device or emulator that is set to a supported locale.</p>
+
+<p>For more information, see <a href="#test-for-default">Testing for Default Resources</a>.</p>
+
+<h4>Design a flexible layout</h4>
+
+<p> If you need to rearrange your layout to fit a certain language (for example
+German with its long words), you can create an alternative layout for that
+language (for example <code>res/layout-de/main.xml</code>). However, doing this
+can make your application harder to maintain. It is better to create a single
+layout that is more flexible.</p>
+
+<p>Another typical situation is a language that requires something different in
+its layout. For example, you might have a contact form that should include two
+name fields when the application runs in Japanese, but three name fields when
+the application runs in some other language. You could handle this in either of
+two ways:</p>
+
+<ul>
+ <li>Create one layout with a field that you can programmatically enable or
+disable, based on the language, or</li>
+ <li>Have the main layout include another layout that includes the changeable
+field. The second layout can have different configurations for different
+languages.</li>
+</ul>
+
+<h4>Avoid creating more resource files and text strings than you need</h4>
+
+<p>You probably do not need to create a locale-specific
+alternative for every resource in your application. For example, the layout
+defined in the <code>res/layout/main.xml</code> file might work in any locale,
+in which case there would be no need to create any alternative layout files.
+</p>
+
+<p>Also, you might not need to create alternative text for every
+string. For example, assume the following:</p>
+
+<ul>
+ <li>Your application's default language is American
+English. Every string that the application uses is defined, using American
+English spellings, in <code>res/values/strings.xml</code>. </li>
+
+ <li>For a few important phrases, you want to provide
+British English spelling. You want these alternative strings to be used when your
+application runs on a device in the United Kingdom. </li>
+</ul>
+
+<p>To do this, you could create a small file called
+<code>res/values-en-rGB/strings.xml</code> that includes only the strings that
+should be different when the application runs in the U.K. For all the rest of
+the strings, the application will fall back to the defaults and use what is
+defined in <code>res/values/strings.xml</code>.</p>
+
+<h4>Use the Android Context object for manual locale lookup</h4>
+
+<p>You can look up the locale using the {@link android.content.Context} object
+that Android makes available:</p>
+
+<pre>String locale = context.getResources().getConfiguration().locale.getDisplayName();</pre>
+
+<h2 id="testing">Testing Localized Applications</h2>
+
+<h3 id="device">Testing on a Device</h3>
+<p>Keep in mind that the device you are testing may be significantly different from
+ the devices available to consumers in other geographies. The locales available
+ on your device may differ from those available on other devices. Also, the
+ resolution and density of the device screen may differ, which could affect
+ the display of strings and drawables in your UI.</p>
+
+<p>To change the locale or language on a device, use the Settings application.</p>
+
+<h3 id="emulator">Testing on an Emulator</h3>
+
+<p>For details about using the emulator, see See <a
+href="{@docRoot}tools/help/emulator.html">Android Emulator</a>.</p>
+<h4>Creating and using a custom locale</h4>
+
+<p>A "custom" locale is a language/region combination that the Android
+system image does not explicitly support. (For a list of supported locales in
+Android platforms see the Version Notes in the <a
+href="{@docRoot}sdk/index.html">SDK</a> tab). You can test
+how your application will run in a custom locale by creating a custom locale in
+the emulator. There are two ways to do this:</p>
+
+<ul>
+ <li>Use the Custom Locale application, which is accessible from the
+Application tab. (After you create a custom locale, switch to it by
+pressing and holding the locale name.)</li>
+ <li>Change to a custom locale from the adb shell, as described below.</li>
+</ul>
+
+<p>When you set the emulator to a locale that is not available in the Android
+system image, the system itself will display in its default language. Your
+application, however, should localize properly.</p>
+
+<h4>Changing the emulator locale from the adb shell</h4>
+
+<p>To change the locale in the emulator by using the adb shell. </p>
+
+<ol>
+ <li>Pick the locale you want to test and determine its BCP-47 language tag, for
+example, Canadian French would be <code>fr-CA</code>.<br>
+ </li>
+ <li>Launch an emulator.</li>
+ <li>From a command-line shell on the host computer, run the following
+command:<br>
+ <code>adb shell</code><br>
+ or if you have a device attached, specify that you want the emulator by adding
+the <code>-e</code> option:<br>
+ <code>adb -e shell</code></li>
+ <li>At the adb shell prompt (<code>#</code>), run this command: <br>
+ <code>setprop persist.sys.locale [<em>BCP-47 language tag</em>];stop;sleep 5;start <br>
+ </code>Replace bracketed sections with the appropriate codes from Step
+1.</li>
+</ol>
+
+<p>For instance, to test in Canadian French:</p>
+
+<p><code>setprop persist.sys.locale fr-CA;stop;sleep 5;start </code></p>
+
+<p>This will cause the emulator to restart. (It will look like a full reboot,
+but it is not.) Once the Home screen appears again, re-launch your application (for
+example, click the Run icon in Eclipse), and the application will launch with
+the new locale. </p>
+
+<h3 id="test-for-default">Testing for Default Resources</h3>
+<p>Here's how to test whether an application includes every string resource that it needs: </p>
+<ol><li>Set the emulator or device to a language that your application does not
+ support. For example, if the application has French strings in
+ <code>res/values-fr/</code> but does not have any Spanish strings in
+ <code>res/values-es/</code>, then set the emulator's locale to Spanish.
+ (You can use the Custom Locale application to set the emulator to an
+ unsupported locale.)</li>
+ <li>Run the application.</li>
+<li>If the application shows an error message and a Force Close button, it might
+ be looking for a string that is not available. Make sure that your
+ <code>res/values/strings.xml</code> file includes a definition for
+ every string that the application uses.</li>
+</ol>
+</p>
+
+<p>If the test is successful, repeat it for other types of
+ configurations. For example, if the application has a layout file called
+ <code>res/layout-land/main.xml</code> but does not contain a file called
+ <code>res/layout-port/main.xml</code>, then set the emulator or device to
+ portrait orientation and see if the application will run.
+
+
+
diff --git a/docs/overview-ext.html b/docs/overview-ext.html
index 5720245..f80d629 100644
--- a/docs/overview-ext.html
+++ b/docs/overview-ext.html
@@ -1,8 +1,8 @@
-<body>
-Some random libraries that we've imported:
-<ul>
- <li>GData</li>
- <li>Apache commons HTTPClient</li>
- <li>A sax parser</li>
-</ul>
-</body>
+<body>
+Some random libraries that we've imported:
+<ul>
+ <li>GData</li>
+ <li>Apache commons HTTPClient</li>
+ <li>A sax parser</li>
+</ul>
+</body>
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index 52fe973..1ccc59a 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -3935,7 +3935,9 @@
if (Res_GETPACKAGE(resID)+1 == 0) {
ALOGW("No package identifier when getting name for resource number 0x%08x", resID);
} else {
+#ifndef STATIC_ANDROIDFW_FOR_TOOLS
ALOGW("No known package when getting name for resource number 0x%08x", resID);
+#endif
}
return false;
}
diff --git a/libs/hwui/JankTracker.cpp b/libs/hwui/JankTracker.cpp
index 76e587e..47d0108 100644
--- a/libs/hwui/JankTracker.cpp
+++ b/libs/hwui/JankTracker.cpp
@@ -15,6 +15,8 @@
*/
#include "JankTracker.h"
+#include "Properties.h"
+
#include <algorithm>
#include <cutils/ashmem.h>
#include <cutils/log.h>
@@ -77,6 +79,11 @@
// If a frame is > this, start counting in increments of 4ms
static const uint32_t kBucket4msIntervals = 48;
+// For testing purposes to try and eliminate test infra overhead we will
+// consider any unknown delay of frame start as part of the test infrastructure
+// and filter it out of the frame profile data
+static FrameInfoIndex sFrameStart = FrameInfoIndex::IntendedVsync;
+
// This will be called every frame, performance sensitive
// Uses bit twiddling to avoid branching while achieving the packing desired
static uint32_t frameCountIndexForFrameTime(nsecs_t frameTime, uint32_t max) {
@@ -242,7 +249,7 @@
mData->totalFrameCount++;
// Fast-path for jank-free frames
int64_t totalDuration =
- frame[FrameInfoIndex::FrameCompleted] - frame[FrameInfoIndex::IntendedVsync];
+ frame[FrameInfoIndex::FrameCompleted] - frame[sFrameStart];
uint32_t framebucket = frameCountIndexForFrameTime(
totalDuration, mData->frameCounts.size() - 1);
// Keep the fast path as fast as possible.
@@ -280,6 +287,9 @@
}
void JankTracker::dumpData(const ProfileData* data, int fd) {
+ if (sFrameStart != FrameInfoIndex::IntendedVsync) {
+ dprintf(fd, "\nNote: Data has been filtered!");
+ }
dprintf(fd, "\nStats since: %" PRIu64 "ns", data->statStartTime);
dprintf(fd, "\nTotal frames rendered: %u", data->totalFrameCount);
dprintf(fd, "\nJanky frames: %u (%.2f%%)", data->jankFrameCount,
@@ -305,6 +315,9 @@
mData->totalFrameCount = 0;
mData->jankFrameCount = 0;
mData->statStartTime = systemTime(CLOCK_MONOTONIC);
+ sFrameStart = Properties::filterOutTestOverhead
+ ? FrameInfoIndex::HandleInputStart
+ : FrameInfoIndex::IntendedVsync;
}
uint32_t JankTracker::findPercentile(const ProfileData* data, int percentile) {
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index bbd8c72..6f68c2b 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -66,6 +66,8 @@
bool Properties::waitForGpuCompletion = false;
+bool Properties::filterOutTestOverhead = false;
+
static int property_get_int(const char* key, int defaultValue) {
char buf[PROPERTY_VALUE_MAX] = {'\0',};
@@ -156,6 +158,8 @@
textureCacheFlushRate = std::max(0.0f, std::min(1.0f,
property_get_float(PROPERTY_TEXTURE_CACHE_FLUSH_RATE, DEFAULT_TEXTURE_CACHE_FLUSH_RATE)));
+ filterOutTestOverhead = property_get_bool(PROPERTY_FILTER_TEST_OVERHEAD, false);
+
return (prevDebugLayersUpdates != debugLayersUpdates)
|| (prevDebugOverdraw != debugOverdraw)
|| (prevDebugStencilClip != debugStencilClip);
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index 249b5b0..171873d 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -151,6 +151,8 @@
*/
#define PROPERTY_ENABLE_PARTIAL_UPDATES "debug.hwui.enable_partial_updates"
+#define PROPERTY_FILTER_TEST_OVERHEAD "debug.hwui.filter_test_overhead"
+
///////////////////////////////////////////////////////////////////////////////
// Runtime configuration properties
///////////////////////////////////////////////////////////////////////////////
@@ -294,6 +296,10 @@
// Should be used only by test apps
static bool waitForGpuCompletion;
+ // Should only be set by automated tests to try and filter out
+ // any overhead they add
+ static bool filterOutTestOverhead;
+
private:
static ProfileType sProfileType;
static bool sDisableProfileBars;
diff --git a/location/java/android/location/GnssStatus.java b/location/java/android/location/GnssStatus.java
index 9c509d6..d76feec 100644
--- a/location/java/android/location/GnssStatus.java
+++ b/location/java/android/location/GnssStatus.java
@@ -121,7 +121,7 @@
* @param satIndex the index of the satellite in the list.
*/
public float getElevationDegrees(int satIndex) {
- return 0f;
+ return mElevations[satIndex];
}
/**
diff --git a/location/java/android/location/INetInitiatedListener.aidl b/location/java/android/location/INetInitiatedListener.aidl
index f2f5a32..fc64dd6 100644
--- a/location/java/android/location/INetInitiatedListener.aidl
+++ b/location/java/android/location/INetInitiatedListener.aidl
@@ -1,26 +1,26 @@
-/*
-**
-** Copyright 2008, The Android Open Source Project
-**
-** Licensed under the Apache License, Version 2.0 (the "License");
-** you may not use this file except in compliance with the License.
-** You may obtain a copy of the License at
-**
-** http://www.apache.org/licenses/LICENSE-2.0
-**
-** Unless required by applicable law or agreed to in writing, software
-** distributed under the License is distributed on an "AS IS" BASIS,
-** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-** See the License for the specific language governing permissions and
-** limitations under the License.
-*/
-
-package android.location;
-
-/**
- * {@hide}
- */
-interface INetInitiatedListener
-{
- boolean sendNiResponse(int notifId, int userResponse);
-}
+/*
+**
+** Copyright 2008, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+
+package android.location;
+
+/**
+ * {@hide}
+ */
+interface INetInitiatedListener
+{
+ boolean sendNiResponse(int notifId, int userResponse);
+}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 3c42161..0a8c692 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -2149,7 +2149,7 @@
}
if (listener != null) {
Log.d(TAG, "AudioManager dispatching onAudioFocusChange("
- + msg.what + ") for " + msg.obj);
+ + msg.arg1 + ") for " + msg.obj);
listener.onAudioFocusChange(msg.arg1);
}
break;
diff --git a/packages/DocumentsUI/res/menu/activity.xml b/packages/DocumentsUI/res/menu/activity.xml
index 95c6f1f..85e7a7a 100644
--- a/packages/DocumentsUI/res/menu/activity.xml
+++ b/packages/DocumentsUI/res/menu/activity.xml
@@ -86,6 +86,10 @@
android:showAsAction="never"
android:visible="false" />
<item
+ android:id="@+id/menu_advanced"
+ android:showAsAction="never"
+ android:visible="false" />
+ <item
android:id="@+id/menu_settings"
android:title="@string/menu_settings"
android:showAsAction="never"
diff --git a/packages/DocumentsUI/res/values/config.xml b/packages/DocumentsUI/res/values/config.xml
index 765211d..6590bbed 100644
--- a/packages/DocumentsUI/res/values/config.xml
+++ b/packages/DocumentsUI/res/values/config.xml
@@ -24,8 +24,9 @@
<!-- Indicates if the home directory should be hidden in the roots list, that is presented
in the drawer/left side panel ) -->
<bool name="home_root_hidden">true</bool>
- <!-- Indicates if the advanced roots like internal storage should be hidden in the roots list) -->
- <bool name="advanced_roots_hidden">true</bool>
+ <!-- Indicates if the advanced roots like internal storage should be shown in the roots list.
+ When enabled there is no menu option to toggle internal storage visibility. -->
+ <bool name="advanced_roots_shown">false</bool>
<!-- Indicates if search view is taking the whole toolbar space -->
<bool name="full_bar_search_view">true</bool>
</resources>
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index b26ee97..fb557ca 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -204,6 +204,9 @@
<string name="open_external_dialog_request">Grant <xliff:g id="appName" example="System Settings"><b>^1</b></xliff:g>
access to <xliff:g id="directory" example="Pictures"><i>^2</i></xliff:g> directory on
<xliff:g id="storage" example="SD Card"><i>^3</i></xliff:g>?</string>
+ <!-- Text in an alert dialog asking user to grant app access to a given directory in the internal storage -->
+ <string name="open_external_dialog_request_primary_volume">Grant <xliff:g id="appName" example="System Settings"><b>^1</b></xliff:g>
+ access to <xliff:g id="directory" example="Pictures"><i>^2</i></xliff:g> directory?</string>
<!-- Text in an alert dialog asking user to grant app access to all data in an external storage volume -->
<string name="open_external_dialog_root_request">Grant <xliff:g id="appName" example="System Settings"><b>^1</b></xliff:g>
access to your data, including photos and videos, on <xliff:g id="storage" example="SD Card"><i>^2</i></xliff:g>?</string>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
index 69315f7..2d051e4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java
@@ -18,6 +18,11 @@
import static com.android.documentsui.Shared.DEBUG;
import static com.android.documentsui.Shared.EXTRA_BENCHMARK;
+import static com.android.documentsui.State.ACTION_CREATE;
+import static com.android.documentsui.State.ACTION_OPEN;
+import static com.android.documentsui.State.ACTION_OPEN_TREE;
+import static com.android.documentsui.State.ACTION_GET_CONTENT;
+import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION;
import static com.android.documentsui.State.MODE_GRID;
import android.app.Activity;
@@ -165,6 +170,7 @@
final MenuItem sortSize = menu.findItem(R.id.menu_sort_size);
final MenuItem grid = menu.findItem(R.id.menu_grid);
final MenuItem list = menu.findItem(R.id.menu_list);
+ final MenuItem advanced = menu.findItem(R.id.menu_advanced);
final MenuItem fileSize = menu.findItem(R.id.menu_file_size);
// Search uses backend ranking; no sorting, recents doesn't support sort.
@@ -176,6 +182,9 @@
grid.setVisible(mState.derivedMode != State.MODE_GRID);
list.setVisible(mState.derivedMode != State.MODE_LIST);
+ advanced.setVisible(mState.showAdvancedOption);
+ advanced.setTitle(mState.showAdvancedOption && mState.showAdvanced
+ ? R.string.menu_advanced_hide : R.string.menu_advanced_show);
fileSize.setTitle(LocalPreferences.getDisplayFileSize(this)
? R.string.menu_file_size_hide : R.string.menu_file_size_show);
@@ -195,25 +204,30 @@
return state;
}
- State state = createSharedState();
- includeState(state);
- if (DEBUG) Log.d(mTag, "Created new state object: " + state);
- return state;
- }
-
- private State createSharedState() {
State state = new State();
final Intent intent = getIntent();
state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
-
state.forceSize = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_FILESIZE, false);
state.showSize = state.forceSize || LocalPreferences.getDisplayFileSize(this);
-
state.initAcceptMimes(intent);
state.excludedAuthorities = getExcludedAuthorities();
+ includeState(state);
+
+ // Advanced roots are shown by deafult without menu option if forced by config or intent.
+ state.showAdvanced = getResources().getBoolean(R.bool.advanced_roots_shown)
+ || intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
+ // Menu option is shown for whitelisted intents if advanced roots are not shown by default.
+ state.showAdvancedOption = !state.showAdvanced &&
+ (state.action == ACTION_OPEN ||
+ state.action == ACTION_CREATE ||
+ state.action == ACTION_OPEN_TREE ||
+ state.action == ACTION_GET_CONTENT);
+
+ if (DEBUG) Log.d(mTag, "Created new state object: " + state);
+
return state;
}
@@ -287,6 +301,10 @@
}
return true;
+ case R.id.menu_advanced:
+ setDisplayAdvancedDevices(!mState.showAdvanced);
+ return true;
+
case R.id.menu_file_size:
setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this));
return true;
@@ -452,6 +470,12 @@
? DocumentsContract.buildRootUri("com.android.providers.downloads.documents",
"downloads")
: DocumentsContract.buildHomeUri();
+ }
+
+ void setDisplayAdvancedDevices(boolean display) {
+ mState.showAdvanced = display;
+ RootsFragment.get(getFragmentManager()).onDisplayStateChanged();
+ invalidateOptionsMenu();
}
void setDisplayFileSize(boolean display) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
index ebc9082..5b5a96e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/CreateDirectoryFragment.java
@@ -153,10 +153,12 @@
if (result != null) {
// Navigate into newly created child
mActivity.onDirectoryCreated(result);
+ Metrics.logCreateDirOperation(getContext());
} else {
- Snackbars.makeSnackbar(mActivity, R.string.create_error, Snackbar.LENGTH_SHORT).show();
+ Snackbars.makeSnackbar(mActivity, R.string.create_error, Snackbar.LENGTH_SHORT)
+ .show();
+ Metrics.logCreateDirError(getContext());
}
-
mActivity.setPending(false);
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DrawerController.java b/packages/DocumentsUI/src/com/android/documentsui/DrawerController.java
index 2dbb730..7a4099a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DrawerController.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DrawerController.java
@@ -18,6 +18,7 @@
import static com.android.documentsui.Shared.DEBUG;
+import android.annotation.IntDef;
import android.app.Activity;
import android.content.Context;
import android.support.v4.app.ActionBarDrawerToggle;
@@ -27,16 +28,44 @@
import android.view.View;
import android.widget.Toolbar;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* A facade over the various pieces comprising "roots fragment in a Drawer".
*
* @see DrawerController#create(DrawerLayout)
*/
abstract class DrawerController implements DrawerListener {
-
public static final String TAG = "DrawerController";
+ // Drawer opening triggered by tapping the navigation icon
+ public static final int OPENED_HAMBURGER = 0;
+ // Drawer opening triggered by swiping right from the edge of the screen
+ public static final int OPENED_SWIPE = 1;
+ // Mostly programmatically forced drawer opening
+ public static final int OPENED_OTHER = 2;
+
+ @IntDef(flag = true, value = {
+ OPENED_HAMBURGER,
+ OPENED_SWIPE,
+ OPENED_OTHER
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Trigger {}
+
+ /**
+ * Toggles the drawer and sets the OPENED_OTHER as the action that causes opening the drawer.
+ * @param open
+ */
abstract void setOpen(boolean open);
+
+ /**
+ * Toggles the drawer.
+ * @param open
+ * @param trigger Indicates what action caused opening the drawer. It is ignored for closing.
+ */
+ abstract void setOpen(boolean open, @Trigger int trigger);
abstract boolean isPresent();
abstract boolean isOpen();
abstract void setTitle(String title);
@@ -92,11 +121,11 @@
* Runtime controller that manages a real drawer.
*/
private static final class RuntimeDrawerController extends DrawerController {
-
private final ActionBarDrawerToggle mToggle;
private DrawerLayout mLayout;
private View mDrawer;
private Toolbar mToolbar;
+ private @Trigger int mTrigger = OPENED_OTHER;
public RuntimeDrawerController(
DrawerLayout layout, View drawer, ActionBarDrawerToggle toggle,
@@ -113,8 +142,14 @@
@Override
void setOpen(boolean open) {
+ setOpen(open, OPENED_OTHER);
+ }
+
+ @Override
+ void setOpen(boolean open, @Trigger int trigger) {
if (open) {
mLayout.openDrawer(mDrawer);
+ mTrigger = trigger;
} else {
mLayout.closeDrawer(mDrawer);
}
@@ -148,16 +183,21 @@
@Override
public void onDrawerOpened(View drawerView) {
mToggle.onDrawerOpened(drawerView);
+ Metrics.logDrawerOpened(mToolbar.getContext(), mTrigger);
}
@Override
public void onDrawerClosed(View drawerView) {
mToggle.onDrawerClosed(drawerView);
+ mTrigger = OPENED_OTHER;
}
@Override
public void onDrawerStateChanged(int newState) {
mToggle.onDrawerStateChanged(newState);
+ if (newState == DrawerLayout.STATE_DRAGGING) {
+ mTrigger = OPENED_SWIPE;
+ }
}
}
@@ -169,6 +209,8 @@
@Override
void setOpen(boolean open) {}
+ @Override
+ void setOpen(boolean open, @Trigger int trigger) {}
@Override
boolean isOpen() {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index 3e04e2a..68c0c2a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -235,7 +235,7 @@
// With new multi-window mode we have to pick how we are launched.
// By default we'd be launched in-place above the existing app.
// By setting launch-to-side ActivityManager will open us to side.
- if (inMultiWindow()) {
+ if (isInMultiWindowMode()) {
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
index 7ad4a09..f1f47c8 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
@@ -66,6 +66,7 @@
private static final String COUNT_FILEOP_EXTERNAL = "docsui_fileop_external";
private static final String COUNT_FILEOP_CANCELED = "docsui_fileop_canceled";
private static final String COUNT_STARTUP_MS = "docsui_startup_ms";
+ private static final String COUNT_DRAWER_OPENED = "docsui_drawer_opened";
// Indices for bucketing roots in the roots histogram. "Other" is the catch-all index for any
// root that is not explicitly recognized by the Metrics code (see {@link
@@ -146,10 +147,14 @@
private static final int FILEOP_MOVE_SYSTEM_PROVIDER = 6; // Move to a system provider.
private static final int FILEOP_MOVE_EXTERNAL_PROVIDER = 7; // Move to a 3rd-party provider.
private static final int FILEOP_DELETE = 8;
+ private static final int FILEOP_RENAME = 9;
+ private static final int FILEOP_CREATE_DIR = 10;
private static final int FILEOP_OTHER_ERROR = 100;
private static final int FILEOP_DELETE_ERROR = 101;
private static final int FILEOP_MOVE_ERROR = 102;
private static final int FILEOP_COPY_ERROR = 103;
+ private static final int FILEOP_RENAME_ERROR = 104;
+ private static final int FILEOP_CREATE_DIR_ERROR = 105;
@IntDef(flag = true, value = {
FILEOP_OTHER,
@@ -160,10 +165,14 @@
FILEOP_MOVE_SYSTEM_PROVIDER,
FILEOP_MOVE_EXTERNAL_PROVIDER,
FILEOP_DELETE,
+ FILEOP_RENAME,
+ FILEOP_CREATE_DIR,
FILEOP_OTHER_ERROR,
FILEOP_COPY_ERROR,
FILEOP_MOVE_ERROR,
- FILEOP_DELETE_ERROR
+ FILEOP_DELETE_ERROR,
+ FILEOP_RENAME_ERROR,
+ FILEOP_CREATE_DIR_ERROR
})
@Retention(RetentionPolicy.SOURCE)
public @interface FileOp {}
@@ -227,6 +236,21 @@
@Retention(RetentionPolicy.SOURCE)
public @interface Provider {}
+ // Codes representing different actions to open the drawer. They are used for bucketing stats in
+ // the COUNT_DRAWER_OPENED histogram.
+ // Do not change or rearrange these values, that will break historical data. Only add to the
+ // list.
+ // Do not use negative numbers or zero; clearcut only handles positive integers.
+ private static final int DRAWER_OPENED_HAMBURGER = 1;
+ private static final int DRAWER_OPENED_SWIPE = 2;
+
+ @IntDef(flag = true, value = {
+ DRAWER_OPENED_HAMBURGER,
+ DRAWER_OPENED_SWIPE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DrawerTrigger {}
+
/**
* Logs when DocumentsUI is started, and how. Call this when DocumentsUI first starts up.
*
@@ -287,6 +311,20 @@
}
/**
+ * Logs a drawer opened event. Call this when the user opens drawer by swipe or by clicking the
+ * hamburger icon.
+ * @param context
+ * @param trigger type of action that opened the drawer
+ */
+ public static void logDrawerOpened(Context context, @DrawerController.Trigger int trigger) {
+ if (trigger == DrawerController.OPENED_HAMBURGER) {
+ logHistogram(context, COUNT_DRAWER_OPENED, DRAWER_OPENED_HAMBURGER);
+ } else if (trigger == DrawerController.OPENED_SWIPE) {
+ logHistogram(context, COUNT_DRAWER_OPENED, DRAWER_OPENED_SWIPE);
+ }
+ }
+
+ /**
* Logs file operation stats. Call this when a file operation has completed. The given
* DocumentInfo is only used to distinguish broad categories of actions (e.g. copying from one
* provider to another vs copying within a given provider). No PII is logged.
@@ -317,6 +355,28 @@
}
/**
+ * Logs create directory operation. It is a part of file operation stats. We do not
+ * differentiate between internal and external locations, all create directory operations are
+ * logged under COUNT_FILEOP_SYSTEM. Call this when a create directory operation has completed.
+ *
+ * @param context
+ */
+ public static void logCreateDirOperation(Context context) {
+ logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_CREATE_DIR);
+ }
+
+ /**
+ * Logs rename file operation. It is a part of file operation stats. We do not differentiate
+ * between internal and external locations, all rename operations are logged under
+ * COUNT_FILEOP_SYSTEM. Call this when a rename file operation has completed.
+ *
+ * @param context
+ */
+ public static void logRenameFileOperation(Context context) {
+ logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_RENAME);
+ }
+
+ /**
* Logs some kind of file operation error. Call this when a file operation (e.g. copy, delete)
* fails.
*
@@ -349,6 +409,28 @@
}
/**
+ * Logs create directory operation error. We do not differentiate between internal and external
+ * locations, all create directory errors are logged under COUNT_FILEOP_SYSTEM. Call this when a
+ * create directory operation fails.
+ *
+ * @param context
+ */
+ public static void logCreateDirError(Context context) {
+ logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_CREATE_DIR);
+ }
+
+ /**
+ * Logs rename file operation error. We do not differentiate between internal and external
+ * locations, all rename errors are logged under COUNT_FILEOP_SYSTEM. Call this
+ * when a rename file operation fails.
+ *
+ * @param context
+ */
+ public static void logRenameFileError(Context context) {
+ logHistogram(context, COUNT_FILEOP_SYSTEM, FILEOP_RENAME_ERROR);
+ }
+
+ /**
* Logs the cancellation of a file operation. Call this when a Job is canceled.
* @param context
* @param operationType
diff --git a/packages/DocumentsUI/src/com/android/documentsui/NavigationView.java b/packages/DocumentsUI/src/com/android/documentsui/NavigationView.java
index 30c1020..f6fe47b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/NavigationView.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/NavigationView.java
@@ -91,7 +91,7 @@
private void onNavigationIconClicked() {
if (mDrawer.isPresent()) {
- mDrawer.setOpen(true);
+ mDrawer.setOpen(true, DrawerController.OPENED_HAMBURGER);
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
index 2fe2756..854be0b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/OpenExternalDirectoryActivity.java
@@ -86,6 +86,7 @@
private static final String EXTRA_VOLUME_LABEL = "com.android.documentsui.VOLUME_LABEL";
private static final String EXTRA_VOLUME_UUID = "com.android.documentsui.VOLUME_UUID";
private static final String EXTRA_IS_ROOT = "com.android.documentsui.IS_ROOT";
+ private static final String EXTRA_IS_PRIMARY = "com.android.documentsui.IS_PRIMARY";
// Special directory name representing the full volume
static final String DIRECTORY_ROOT = "ROOT_DIRECTORY";
@@ -157,6 +158,13 @@
Log.d(TAG, "showFragment() for volume " + storageVolume.dump() + ", directory "
+ directoryName + ", and user " + userId);
final boolean isRoot = directoryName.equals(DIRECTORY_ROOT);
+ final boolean isPrimary = storageVolume.isPrimary();
+
+ if (isRoot && isPrimary) {
+ if (DEBUG) Log.d(TAG, "root access requested on primary volume");
+ return false;
+ }
+
final File volumeRoot = storageVolume.getPathFile();
File file;
try {
@@ -235,6 +243,7 @@
args.putString(EXTRA_VOLUME_UUID, volumeUuid);
args.putString(EXTRA_APP_LABEL, appLabel);
args.putBoolean(EXTRA_IS_ROOT, isRoot);
+ args.putBoolean(EXTRA_IS_PRIMARY, isPrimary);
final FragmentManager fm = activity.getFragmentManager();
final FragmentTransaction ft = fm.beginTransaction();
@@ -352,6 +361,7 @@
private String mVolumeLabel;
private String mAppLabel;
private boolean mIsRoot;
+ private boolean mIsPrimary;
private CheckBox mDontAskAgain;
private OpenExternalDirectoryActivity mActivity;
private AlertDialog mDialog;
@@ -367,6 +377,7 @@
mVolumeLabel = args.getString(EXTRA_VOLUME_LABEL);
mAppLabel = args.getString(EXTRA_APP_LABEL);
mIsRoot = args.getBoolean(EXTRA_IS_ROOT);
+ mIsPrimary= args.getBoolean(EXTRA_IS_PRIMARY);
}
mActivity = (OpenExternalDirectoryActivity) getActivity();
}
@@ -435,7 +446,9 @@
message = TextUtils.expandTemplate(getText(
R.string.open_external_dialog_root_request), mAppLabel, mVolumeLabel);
} else {
- message = TextUtils.expandTemplate(getText(R.string.open_external_dialog_request),
+ message = TextUtils.expandTemplate(
+ getText(mIsPrimary ? R.string.open_external_dialog_request_primary_volume
+ : R.string.open_external_dialog_request),
mAppLabel, directory, mVolumeLabel);
}
final TextView messageField = (TextView) view.findViewById(R.id.message);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
index 6efe9c8..09fadc9 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsCache.java
@@ -65,8 +65,6 @@
private static final String TAG = "RootsCache";
- private static final boolean ENABLE_SYSTEM_CACHE = true;
-
private final Context mContext;
private final ContentObserver mObserver;
private OnCacheUpdateListener mCacheUpdateListener;
@@ -200,7 +198,7 @@
synchronized (mLock) {
for (String authority : mStoppedAuthorities) {
if (DEBUG) Log.d(TAG, "Loading stopped authority " + authority);
- mRoots.putAll(authority, loadRootsForAuthority(resolver, authority));
+ mRoots.putAll(authority, loadRootsForAuthority(resolver, authority, true));
}
mStoppedAuthorities.clear();
}
@@ -219,13 +217,13 @@
if (DEBUG) {
Log.d(TAG, "Loading stopped authority " + authority);
}
- mRoots.putAll(authority, loadRootsForAuthority(resolver, authority));
+ mRoots.putAll(authority, loadRootsForAuthority(resolver, authority, true));
mStoppedAuthorities.remove(authority);
}
}
private class UpdateTask extends AsyncTask<Void, Void, Void> {
- private final String mFilterPackage;
+ private final String mForceRefreshPackage;
private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create();
private final HashSet<String> mTaskStoppedAuthorities = new HashSet<>();
@@ -238,18 +236,18 @@
}
/**
- * Only update roots belonging to given package name. Other roots will
+ * Force update roots belonging to given package name. Other roots will
* be copied from cached {@link #mRoots} values.
*/
- public UpdateTask(String filterPackage) {
- mFilterPackage = filterPackage;
+ public UpdateTask(String forceRefreshPackage) {
+ mForceRefreshPackage = forceRefreshPackage;
}
@Override
protected Void doInBackground(Void... params) {
final long start = SystemClock.elapsedRealtime();
- if (mFilterPackage != null) {
+ if (mForceRefreshPackage != null) {
// We must have previously cached values to fill in non-matching
// packages, so wait around for successful first load.
if (!waitForFirstLoad()) {
@@ -302,29 +300,17 @@
return;
}
- // Try using cached roots if filtering
- boolean cacheHit = false;
- if (mFilterPackage != null && !mFilterPackage.equals(info.packageName)) {
- synchronized (mLock) {
- if (mTaskRoots.putAll(info.authority, mRoots.get(info.authority))) {
- if (DEBUG) Log.d(TAG, "Used cached roots for " + info.authority);
- cacheHit = true;
- }
- }
- }
-
- // Cache miss, or loading everything
- if (!cacheHit) {
- mTaskRoots.putAll(info.authority,
- loadRootsForAuthority(mContext.getContentResolver(), info.authority));
- }
+ final boolean forceRefresh = Objects.equals(mForceRefreshPackage, info.packageName);
+ mTaskRoots.putAll(info.authority, loadRootsForAuthority(mContext.getContentResolver(),
+ info.authority, forceRefresh));
}
}
/**
* Bring up requested provider and query for all active roots.
*/
- private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority) {
+ private Collection<RootInfo> loadRootsForAuthority(ContentResolver resolver, String authority,
+ boolean forceRefresh) {
if (DEBUG) Log.d(TAG, "Loading roots for " + authority);
synchronized (mObservedAuthorities) {
@@ -336,7 +322,7 @@
}
final Uri rootsUri = DocumentsContract.buildRootsUri(authority);
- if (ENABLE_SYSTEM_CACHE) {
+ if (!forceRefresh) {
// Look for roots data that we might have cached for ourselves in the
// long-lived system process.
final Bundle systemCache = resolver.getCache(rootsUri);
@@ -363,14 +349,12 @@
ContentProviderClient.releaseQuietly(client);
}
- if (ENABLE_SYSTEM_CACHE) {
- // Cache these freshly parsed roots over in the long-lived system
- // process, in case our process goes away. The system takes care of
- // invalidating the cache if the package or Uri changes.
- final Bundle systemCache = new Bundle();
- systemCache.putParcelableArrayList(TAG, roots);
- resolver.putCache(rootsUri, systemCache);
- }
+ // Cache these freshly parsed roots over in the long-lived system
+ // process, in case our process goes away. The system takes care of
+ // invalidating the cache if the package or Uri changes.
+ final Bundle systemCache = new Bundle();
+ systemCache.putParcelableArrayList(TAG, roots);
+ resolver.putCache(rootsUri, systemCache);
return roots;
}
@@ -384,8 +368,8 @@
synchronized (mLock) {
RootInfo root = getRootLocked(authority, rootId);
if (root == null) {
- mRoots.putAll(
- authority, loadRootsForAuthority(mContext.getContentResolver(), authority));
+ mRoots.putAll(authority,
+ loadRootsForAuthority(mContext.getContentResolver(), authority, false));
root = getRootLocked(authority, rootId);
}
return root;
@@ -493,6 +477,11 @@
continue;
}
+ if (!state.showAdvanced && root.isAdvanced()) {
+ if (DEBUG) Log.d(TAG, "Excluding root because: unwanted advanced device.");
+ continue;
+ }
+
if (state.localOnly && !root.isLocalOnly()) {
if (DEBUG) Log.d(TAG, "Excluding root because: unwanted non-local device.");
continue;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index 8b4f40e..5f665c0 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -321,9 +321,6 @@
if (root.isHome() && Shared.isHomeRootHidden(context)) {
continue;
- } else if (root.isAdvanced()
- && Shared.areAdvancedRootsHidden(context, state)) {
- continue;
} else if (root.isLibrary()) {
if (DEBUG) Log.d(TAG, "Adding " + root + " as library.");
libraries.add(item);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Shared.java b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
index 655359a..2c60d4a 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Shared.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
@@ -181,12 +181,4 @@
return context.getResources().getBoolean(R.bool.home_root_hidden);
}
- /*
- * Indicates if the advanced roots should be hidden.
- */
- public static boolean areAdvancedRootsHidden(Context context, State state) {
- return context.getResources().getBoolean(R.bool.advanced_roots_hidden)
- && state.action != ACTION_OPEN_TREE;
- }
-
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/State.java b/packages/DocumentsUI/src/com/android/documentsui/State.java
index 534a483..c7d60e3 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/State.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/State.java
@@ -93,6 +93,8 @@
public boolean forceSize;
public boolean showSize;
public boolean localOnly;
+ public boolean showAdvancedOption;
+ public boolean showAdvanced;
public boolean restored;
/*
* Indicates handler was an external app, like photos.
@@ -194,6 +196,8 @@
out.writeInt(forceSize ? 1 : 0);
out.writeInt(showSize ? 1 : 0);
out.writeInt(localOnly ? 1 : 0);
+ out.writeInt(showAdvancedOption ? 1 : 0);
+ out.writeInt(showAdvanced ? 1 : 0);
out.writeInt(restored ? 1 : 0);
out.writeInt(external ? 1 : 0);
DurableUtils.writeToParcel(out, stack);
@@ -223,6 +227,8 @@
state.forceSize = in.readInt() != 0;
state.showSize = in.readInt() != 0;
state.localOnly = in.readInt() != 0;
+ state.showAdvancedOption = in.readInt() != 0;
+ state.showAdvanced = in.readInt() != 0;
state.restored = in.readInt() != 0;
state.external = in.readInt() != 0;
DurableUtils.readFromParcel(in, state.stack);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 4acf85e..73ce0e1 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -1084,7 +1084,7 @@
}
// Make all items draggable.
- view.setOnLongClickListener(mDragHelper);
+ view.setOnLongClickListener(onLongClickListener);
}
private View.OnDragListener mOnDragListener = new View.OnDragListener() {
@@ -1387,9 +1387,10 @@
}
}
- private DragStartHelper mDragHelper = new DragStartHelper(null) {
+ private DragStartHelper.OnDragStartListener mOnDragStartListener =
+ new DragStartHelper.OnDragStartListener() {
@Override
- protected boolean onDragStart(View v) {
+ public boolean onDragStart(View v, DragStartHelper helper) {
if (isSelected(getModelId(v))) {
List<DocumentInfo> docs = getDraggableDocuments(v);
if (docs.isEmpty()) {
@@ -1409,6 +1410,15 @@
}
};
+ private DragStartHelper mDragHelper = new DragStartHelper(null, mOnDragStartListener);
+
+ private View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ return mDragHelper.onLongClick(v);
+ }
+ };
+
// Previously we listened to events with one class, only to bounce them forward
// to GestureDetector. We're still doing that here, but with a single class
// that reduces overall complexity in our glue code.
@@ -1439,7 +1449,7 @@
// Detect drag events. When a drag is detected, intercept the rest of the gesture.
View itemView = rv.findChildViewUnder(e.getX(), e.getY());
- if (itemView != null && mDragHelper.handleTouch(itemView, e)) {
+ if (itemView != null && mDragHelper.onTouch(itemView, e)) {
return true;
}
// Forward unhandled events to the GestureDetector.
@@ -1451,7 +1461,7 @@
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
View itemView = rv.findChildViewUnder(e.getX(), e.getY());
- mDragHelper.handleTouch(itemView, e);
+ mDragHelper.onTouch(itemView, e);
// Note: even though this event is being handled as part of a drag gesture, continue
// forwarding to the GestureDetector. The detector needs to see the entire cluster of
// events in order to properly interpret gestures.
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/RenameDocumentFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
index 884abbb..73aa366 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/RenameDocumentFragment.java
@@ -44,6 +44,7 @@
import com.android.documentsui.BaseActivity;
import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.Metrics;
import com.android.documentsui.R;
import com.android.documentsui.Shared;
import com.android.documentsui.Snackbars;
@@ -227,11 +228,13 @@
@Override
protected void onPostExecute(DocumentInfo result) {
- if (result == null) {
+ if (result != null) {
+ Metrics.logRenameFileOperation(getContext());
+ } else {
Snackbars.makeSnackbar(mActivity, R.string.rename_error, Snackbar.LENGTH_SHORT)
.show();
+ Metrics.logRenameFileError(getContext());
}
-
mActivity.setPending(false);
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/RootsCacheTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/RootsCacheTest.java
index e73dd8c..2e81545 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/RootsCacheTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/RootsCacheTest.java
@@ -55,6 +55,7 @@
mState = new State();
mState.action = State.ACTION_OPEN;
+ mState.showAdvanced = true;
mState.localOnly = false;
}
diff --git a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityContainer.java
index b1d42e7..a7e4e12 100644
--- a/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/Keyguard/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -464,7 +464,7 @@
return 0;
}
- private int getLayoutIdFor(SecurityMode securityMode) {
+ protected int getLayoutIdFor(SecurityMode securityMode) {
switch (securityMode) {
case Pattern: return R.layout.keyguard_pattern_view;
case PIN: return R.layout.keyguard_pin_view;
diff --git a/packages/PrintServiceRecommendationService/Android.mk b/packages/PrintServiceRecommendationService/Android.mk
new file mode 100644
index 0000000..66cb057
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/Android.mk
@@ -0,0 +1,29 @@
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := PrintRecommendationService
+
+include $(BUILD_PACKAGE)
+
+LOCAL_SDK_VERSION := system_current
+
+include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/packages/PrintServiceRecommendationService/AndroidManifest.xml b/packages/PrintServiceRecommendationService/AndroidManifest.xml
new file mode 100644
index 0000000..0eb218c
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (c) 2016 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.printservice.recommendation">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:allowClearUserData="false"
+ android:label="@string/app_label"
+ android:allowBackup= "false">
+
+ <service
+ android:name=".RecommendationServiceImpl"
+ android:permission="android.permission.BIND_PRINT_RECOMMENDATION_SERVICE">
+
+ <intent-filter>
+ <action android:name="android.printservice.recommendation.RecommendationService" />
+ </intent-filter>
+ </service>
+
+ </application>
+
+</manifest>
diff --git a/packages/PrintServiceRecommendationService/MODULE_LICENSE_APACHE2 b/packages/PrintServiceRecommendationService/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/MODULE_LICENSE_APACHE2
diff --git a/packages/PrintServiceRecommendationService/NOTICE b/packages/PrintServiceRecommendationService/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/packages/PrintServiceRecommendationService/res/values/donottranslate.xml b/packages/PrintServiceRecommendationService/res/values/donottranslate.xml
new file mode 100644
index 0000000..4cf0eaf
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/res/values/donottranslate.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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>
+ <string name="app_label">Print Service Recommendation Service</string>
+</resources>
diff --git a/packages/PrintServiceRecommendationService/res/values/strings.xml b/packages/PrintServiceRecommendationService/res/values/strings.xml
new file mode 100644
index 0000000..83d3800
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/res/values/strings.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ (c) Copyright 2016 Mopria Alliance, Inc.
+ (c) Copyright 2016 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>
+ <string name="plugin_vendor_hp">HP</string>
+ <string name="plugin_vendor_lexmark">Lexmark</string>
+ <string name="plugin_vendor_brother">Brother</string>
+ <string name="plugin_vendor_canon">Canon</string>
+ <string name="plugin_vendor_xerox">Xerox</string>
+ <string name="plugin_vendor_samsung">Samsung Electorics</string>
+ <string name="plugin_vendor_epson">Epson</string>
+ <string name="plugin_vendor_konika_minolta">Konika Minolta</string>
+ <string name="plugin_vendor_fuji">Fuji</string>
+</resources>
diff --git a/packages/PrintServiceRecommendationService/res/xml/vendorconfigs.xml b/packages/PrintServiceRecommendationService/res/xml/vendorconfigs.xml
new file mode 100644
index 0000000..fda2768
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/res/xml/vendorconfigs.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ (c) Copyright 2016 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.
+ -->
+
+<vendors>
+ <vendor>
+ <name>@string/plugin_vendor_hp</name>
+ <package>com.hp.android.printservice</package>
+ <mdns-names>
+ <mdns-name>HP</mdns-name>
+ <mdns-name>Hewlett-Packard</mdns-name>
+ <mdns-name>Hewlett Packard</mdns-name>
+ </mdns-names>
+ </vendor>
+
+ <vendor>
+ <name>@string/plugin_vendor_lexmark</name>
+ <package>com.lexmark.print.plugin</package>
+ <mdns-names>
+ <mdns-name>Lexmark</mdns-name>
+ <mdns-name>Lexmark International</mdns-name>
+ </mdns-names>
+ </vendor>
+
+ <vendor>
+ <name>@string/plugin_vendor_brother</name>
+ <package>com.brother.printservice</package>
+ <mdns-names>
+ <mdns-name>Brother</mdns-name>
+ </mdns-names>
+ </vendor>
+
+ <vendor>
+ <name>@string/plugin_vendor_canon</name>
+ <package>com.xerox.printservice</package>
+ <mdns-names>
+ <mdns-name>Canon</mdns-name>
+ </mdns-names>
+ </vendor>
+
+ <vendor>
+ <name>@string/plugin_vendor_xerox</name>
+ <package>jp.co.canon.android.printservice.plugin</package>
+ <mdns-names>
+ <mdns-name>Xerox</mdns-name>
+ </mdns-names>
+ </vendor>
+
+ <vendor>
+ <name>@string/plugin_vendor_samsung</name>
+ <package>com.sec.app.samsungprintservice</package>
+ <mdns-names>
+ <mdns-name>Samsung</mdns-name>
+ </mdns-names>
+ </vendor>
+
+ <vendor>
+ <name>@string/plugin_vendor_epson</name>
+ <package>com.epson.mobilephone.android.epsonprintserviceplugin</package>
+ <mdns-names>
+ <mdns-name>Epson</mdns-name>
+ </mdns-names>
+ </vendor>
+
+ <vendor>
+ <name>@string/plugin_vendor_konika_minolta</name>
+ <package>com.kmbt.printservice</package>
+ <mdns-names>
+ <mdns-name>kmkmkm</mdns-name>
+ <mdns-name>Konica Minolta</mdns-name>
+ <mdns-name>Minolta</mdns-name>
+ </mdns-names>
+ </vendor>
+
+ <vendor>
+ <name>@string/plugin_vendor_fuji</name>
+ <package>jp.co.fujixerox.prt.PrintUtil.PCL</package>
+ <mdns-names>
+ <mdns-name>FUJI XEROX</mdns-name>
+ </mdns-names>
+ </vendor>
+</vendors>
diff --git a/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/PrintServicePlugin.java b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/PrintServicePlugin.java
new file mode 100644
index 0000000..d604ef8
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/PrintServicePlugin.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.StringRes;
+
+/**
+ * Interface to be implemented by each print service plugin.
+ * <p/>
+ * A print service plugin is a minimal version of a real {@link android.printservice.PrintService
+ * print service}. You cannot print using the plugin. The only functionality in the plugin is to
+ * report the number of printers that the real service would discover.
+ */
+public interface PrintServicePlugin {
+ /**
+ * Call back used by the print service plugins.
+ */
+ interface PrinterDiscoveryCallback {
+ /**
+ * Announce that something changed and the UI for this plugin should be updated.
+ *
+ * @param numDiscoveredPrinters The number of printers discovered.
+ */
+ void onChanged(@IntRange(from = 0) int numDiscoveredPrinters);
+ }
+
+ /**
+ * Get the name (a string reference) of the {@link android.printservice.PrintService print
+ * service} with the {@link #getPackageName specified package name}. This is read once, hence
+ * returning different data at different times is not allowed.
+ *
+ * @return The name of the print service as a string reference. The localization is handled
+ * outside of the plugin.
+ */
+ @StringRes int getName();
+
+ /**
+ * The package name of the full print service.
+ *
+ * @return The package name
+ */
+ @NonNull CharSequence getPackageName();
+
+ /**
+ * Start the discovery plugin.
+ *
+ * @param callback Callbacks used by this plugin.
+ *
+ * @throws Exception If anything went wrong when starting the plugin
+ */
+ void start(@NonNull PrinterDiscoveryCallback callback) throws Exception;
+
+ /**
+ * Stop the plugin. This can only return once the plugin is completely finished and cleaned up.
+ *
+ * @throws Exception If anything went wrong while stopping plugin
+ */
+ void stop() throws Exception;
+}
diff --git a/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
new file mode 100644
index 0000000..9f6dad8
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/RecommendationServiceImpl.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation;
+
+import android.content.res.Configuration;
+import android.printservice.recommendation.RecommendationInfo;
+import android.printservice.recommendation.RecommendationService;
+import android.printservice.PrintService;
+import android.util.Log;
+import com.android.printservice.recommendation.plugin.mdnsFilter.MDNSFilterPlugin;
+import com.android.printservice.recommendation.plugin.mdnsFilter.VendorConfig;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * Service that recommends {@link PrintService print services} that might be a good idea to install.
+ */
+public class RecommendationServiceImpl extends RecommendationService
+ implements RemotePrintServicePlugin.OnChangedListener {
+ private static final String LOG_TAG = "PrintServiceRecService";
+
+ /** All registered plugins */
+ private ArrayList<RemotePrintServicePlugin> mPlugins;
+
+ @Override
+ public void onConnected() {
+ mPlugins = new ArrayList<>();
+
+ try {
+ for (VendorConfig config : VendorConfig.getAllConfigs(this)) {
+ try {
+ mPlugins.add(new RemotePrintServicePlugin(new MDNSFilterPlugin(this,
+ config.name, config.packageName, config.mDNSNames), this, false));
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Could not initiate simple MDNS plugin for " +
+ config.packageName, e);
+ }
+ }
+ } catch (IOException | XmlPullParserException e) {
+ new RuntimeException("Could not parse vendorconfig", e);
+ }
+
+ final int numPlugins = mPlugins.size();
+ for (int i = 0; i < numPlugins; i++) {
+ try {
+ mPlugins.get(i).start();
+ } catch (RemotePrintServicePlugin.PluginException e) {
+ Log.e(LOG_TAG, "Could not start plugin", e);
+ }
+ }
+ }
+
+ @Override
+ public void onDisconnected() {
+ final int numPlugins = mPlugins.size();
+ for (int i = 0; i < numPlugins; i++) {
+ try {
+ mPlugins.get(i).stop();
+ } catch (RemotePrintServicePlugin.PluginException e) {
+ Log.e(LOG_TAG, "Could not stop plugin", e);
+ }
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ // Need to update plugin names as they might be localized
+ onChanged();
+ }
+
+ @Override
+ public void onChanged() {
+ ArrayList<RecommendationInfo> recommendations = new ArrayList<>();
+
+ final int numPlugins = mPlugins.size();
+ for (int i = 0; i < numPlugins; i++) {
+ RemotePrintServicePlugin plugin = mPlugins.get(i);
+
+ try {
+ int numPrinters = plugin.getNumPrinters();
+
+ if (numPrinters > 0) {
+ recommendations.add(new RecommendationInfo(plugin.packageName,
+ getString(plugin.name), numPrinters,
+ plugin.recommendsMultiVendorService));
+ }
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Could not read state of plugin for " + plugin.packageName, e);
+ }
+ }
+
+ updateRecommendations(recommendations);
+ }
+}
diff --git a/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/RemotePrintServicePlugin.java b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/RemotePrintServicePlugin.java
new file mode 100644
index 0000000..dbd1649
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/RemotePrintServicePlugin.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.StringRes;
+import com.android.internal.util.Preconditions;
+
+/**
+ * Wrapper for a {@link PrintServicePlugin}, isolating issues with the plugin as good as possible
+ * from the {@link RecommendationServiceImpl service}.
+ */
+class RemotePrintServicePlugin implements PrintServicePlugin.PrinterDiscoveryCallback {
+ /** Lock for this object */
+ private final Object mLock = new Object();
+
+ /** The name of the print service. */
+ public final @StringRes int name;
+
+ /** If the print service if for more than a single vendor */
+ public final boolean recommendsMultiVendorService;
+
+ /** The package name of the full print service */
+ public final @NonNull CharSequence packageName;
+
+ /** Wrapped plugin */
+ private final @NonNull PrintServicePlugin mPlugin;
+
+ /** The number of printers discovered by the plugin */
+ private @IntRange(from = 0) int mNumPrinters;
+
+ /** If the plugin is started by not yet stopped */
+ private boolean isRunning;
+
+ /** Listener for changes to {@link #mNumPrinters}. */
+ private @NonNull OnChangedListener mListener;
+
+ /**
+ * Create a new remote for a {@link PrintServicePlugin plugin}.
+ *
+ * @param plugin The plugin to be wrapped
+ * @param listener The listener to be notified about changes in this plugin
+ * @param recommendsMultiVendorService If the plugin detects printers of more than a single
+ * vendor
+ *
+ * @throws PluginException If the plugin has issues while caching basic stub properties
+ */
+ public RemotePrintServicePlugin(@NonNull PrintServicePlugin plugin,
+ @NonNull OnChangedListener listener, boolean recommendsMultiVendorService)
+ throws PluginException {
+ mListener = listener;
+ mPlugin = plugin;
+ this.recommendsMultiVendorService = recommendsMultiVendorService;
+
+ // We handle any throwable to isolate our self from bugs in the plugin code.
+ // Cache simple properties to avoid having to deal with exceptions later in the code.
+ try {
+ name = Preconditions.checkArgumentPositive(mPlugin.getName(), "name");
+ packageName = Preconditions.checkStringNotEmpty(mPlugin.getPackageName(),
+ "packageName");
+ } catch (Throwable e) {
+ throw new PluginException(mPlugin, "Cannot cache simple properties ", e);
+ }
+
+ isRunning = false;
+ }
+
+ /**
+ * Start the plugin. From now on there might be callbacks to the registered listener.
+ */
+ public void start()
+ throws PluginException {
+ // We handle any throwable to isolate our self from bugs in the stub code
+ try {
+ synchronized (mLock) {
+ isRunning = true;
+ mPlugin.start(this);
+ }
+ } catch (Throwable e) {
+ throw new PluginException(mPlugin, "Cannot start", e);
+ }
+ }
+
+ /**
+ * Stop the plugin. From this call on there will not be any more callbacks.
+ */
+ public void stop() throws PluginException {
+ // We handle any throwable to isolate our self from bugs in the stub code
+ try {
+ synchronized (mLock) {
+ mPlugin.stop();
+ isRunning = false;
+ }
+ } catch (Throwable e) {
+ throw new PluginException(mPlugin, "Cannot stop", e);
+ }
+ }
+
+ /**
+ * Get the current number of printers reported by the stub.
+ *
+ * @return The number of printers reported by the stub.
+ */
+ public @IntRange(from = 0) int getNumPrinters() {
+ return mNumPrinters;
+ }
+
+ @Override
+ public void onChanged(@IntRange(from = 0) int numDiscoveredPrinters) {
+ synchronized (mLock) {
+ Preconditions.checkState(isRunning);
+
+ mNumPrinters = Preconditions.checkArgumentNonnegative(numDiscoveredPrinters,
+ "numDiscoveredPrinters");
+
+ if (mNumPrinters > 0) {
+ mListener.onChanged();
+ }
+ }
+ }
+
+ /**
+ * Listener to listen for changes to {@link #getNumPrinters}
+ */
+ public interface OnChangedListener {
+ void onChanged();
+ }
+
+ /**
+ * Exception thrown if the stub has any issues.
+ */
+ public class PluginException extends Exception {
+ private PluginException(PrintServicePlugin plugin, String message, Throwable e) {
+ super(plugin + ": " + message, e);
+ }
+ }
+}
diff --git a/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java
new file mode 100644
index 0000000..26300b1
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/MDNSFilterPlugin.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation.plugin.mdnsFilter;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.content.Context;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.printservice.recommendation.PrintServicePlugin;
+import com.android.printservice.recommendation.util.MDNSUtils;
+import com.android.printservice.recommendation.util.NsdResolveQueue;
+
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * A plugin listening for mDNS results and only adding the ones that {@link
+ * MDNSUtils#isVendorPrinter match} configured list
+ */
+public class MDNSFilterPlugin implements PrintServicePlugin, NsdManager.DiscoveryListener {
+ private static final String LOG_TAG = "MDNSFilterPlugin";
+
+ private static final String PRINTER_SERVICE_TYPE = "_ipp._tcp";
+
+ /** Name of the print service this plugin is for */
+ private final @StringRes int mName;
+
+ /** Package name of the print service this plugin is for */
+ private final @NonNull CharSequence mPackageName;
+
+ /** mDNS names handled by the print service this plugin is for */
+ private final @NonNull HashSet<String> mMDNSNames;
+
+ /** Printer identifiers of the mPrinters found. */
+ @GuardedBy("mLock")
+ private final @NonNull HashSet<String> mPrinters;
+
+ /** Context of the user of this plugin */
+ private final @NonNull Context mContext;
+
+ /**
+ * Call back to report the number of mPrinters found.
+ *
+ * We assume that {@link #start} and {@link #stop} are never called in parallel, hence it is
+ * safe to not synchronize access to this field.
+ */
+ private @Nullable PrinterDiscoveryCallback mCallback;
+
+ /** Queue used to resolve nsd infos */
+ private final @NonNull NsdResolveQueue mResolveQueue;
+
+ /**
+ * Create new stub that assumes that a print service can be used to print on all mPrinters
+ * matching some mDNS names.
+ *
+ * @param context The context the plugin runs in
+ * @param name The user friendly name of the print service
+ * @param packageName The package name of the print service
+ * @param mDNSNames The mDNS names of the printer.
+ */
+ public MDNSFilterPlugin(@NonNull Context context, @NonNull String name,
+ @NonNull CharSequence packageName, @NonNull List<String> mDNSNames) {
+ mContext = Preconditions.checkNotNull(context, "context");
+ mName = mContext.getResources().getIdentifier(Preconditions.checkStringNotEmpty(name,
+ "name"), null, mContext.getPackageName());
+ mPackageName = Preconditions.checkStringNotEmpty(packageName);
+ mMDNSNames = new HashSet<>(Preconditions
+ .checkCollectionNotEmpty(Preconditions.checkCollectionElementsNotNull(mDNSNames,
+ "mDNSNames"), "mDNSNames"));
+
+ mResolveQueue = NsdResolveQueue.getInstance();
+ mPrinters = new HashSet<>();
+ }
+
+ @Override
+ public @NonNull CharSequence getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * @return The NDS manager
+ */
+ private NsdManager getNDSManager() {
+ return (NsdManager) mContext.getSystemService(Context.NSD_SERVICE);
+ }
+
+ @Override
+ public void start(@NonNull PrinterDiscoveryCallback callback) throws Exception {
+ mCallback = callback;
+
+ getNDSManager().discoverServices(PRINTER_SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD,
+ this);
+ }
+
+ @Override
+ public @StringRes int getName() {
+ return mName;
+ }
+
+ @Override
+ public void stop() throws Exception {
+ mCallback.onChanged(0);
+ mCallback = null;
+
+ getNDSManager().stopServiceDiscovery(this);
+ }
+
+ @Override
+ public void onStartDiscoveryFailed(String serviceType, int errorCode) {
+ Log.w(LOG_TAG, "Failed to start network discovery for type " + serviceType + ": "
+ + errorCode);
+ }
+
+ @Override
+ public void onStopDiscoveryFailed(String serviceType, int errorCode) {
+ Log.w(LOG_TAG, "Failed to stop network discovery for type " + serviceType + ": "
+ + errorCode);
+ }
+
+ @Override
+ public void onDiscoveryStarted(String serviceType) {
+ // empty
+ }
+
+ @Override
+ public void onDiscoveryStopped(String serviceType) {
+ mPrinters.clear();
+ }
+
+ @Override
+ public void onServiceFound(NsdServiceInfo serviceInfo) {
+ mResolveQueue.resolve(getNDSManager(), serviceInfo,
+ new NsdManager.ResolveListener() {
+ @Override
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ Log.w(LOG_TAG, "Service found: could not resolve " + serviceInfo + ": " +
+ errorCode);
+ }
+
+ @Override
+ public void onServiceResolved(NsdServiceInfo serviceInfo) {
+ if (MDNSUtils.isVendorPrinter(serviceInfo, mMDNSNames)) {
+ if (mCallback != null) {
+ boolean added = mPrinters.add(serviceInfo.getHost().getHostAddress());
+
+ if (added) {
+ mCallback.onChanged(mPrinters.size());
+ }
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onServiceLost(NsdServiceInfo serviceInfo) {
+ mResolveQueue.resolve(getNDSManager(), serviceInfo,
+ new NsdManager.ResolveListener() {
+ @Override
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ Log.w(LOG_TAG, "Service lost: Could not resolve " + serviceInfo + ": "
+ + errorCode);
+ }
+
+ @Override
+ public void onServiceResolved(NsdServiceInfo serviceInfo) {
+ if (MDNSUtils.isVendorPrinter(serviceInfo, mMDNSNames)) {
+ if (mCallback != null) {
+ boolean removed = mPrinters
+ .remove(serviceInfo.getHost().getHostAddress());
+
+ if (removed) {
+ mCallback.onChanged(mPrinters.size());
+ }
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/VendorConfig.java b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/VendorConfig.java
new file mode 100644
index 0000000..57d5c71
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/plugin/mdnsFilter/VendorConfig.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation.plugin.mdnsFilter;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.XmlResourceParser;
+import android.util.ArrayMap;
+import com.android.internal.annotations.Immutable;
+import com.android.internal.util.Preconditions;
+import com.android.printservice.recommendation.R;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Vendor configuration as read from {@link R.xml#vendorconfigs vendorconfigs.xml}. Configuration
+ * can be read via {@link #getConfig(Context, String)}.
+ */
+@Immutable
+public class VendorConfig {
+ /** Lock for {@link #sConfigs} */
+ private static final Object sLock = new Object();
+
+ /** Strings used as XML tags */
+ private static final String VENDORS_TAG = "vendors";
+ private static final String VENDOR_TAG = "vendor";
+ private static final String NAME_TAG = "name";
+ private static final String PACKAGE_TAG = "package";
+ private static final String MDNSNAMES_TAG = "mdns-names";
+ private static final String MDNSNAME_TAG = "mdns-name";
+
+ /** Map from vendor name to config. Initialized on first {@link #getConfig use}. */
+ private static @Nullable ArrayMap<String, VendorConfig> sConfigs;
+
+ /** Localized vendor name */
+ public final @NonNull String name;
+
+ /** Package name containing the print service for this vendor */
+ public final @NonNull String packageName;
+
+ /** mDNS names used by this vendor */
+ public final @NonNull List<String> mDNSNames;
+
+ /**
+ * Create an immutable configuration.
+ */
+ private VendorConfig(@NonNull String name, @NonNull String packageName,
+ @NonNull List<String> mDNSNames) {
+ this.name = Preconditions.checkStringNotEmpty(name);
+ this.packageName = Preconditions.checkStringNotEmpty(packageName);
+ this.mDNSNames = Preconditions.checkCollectionElementsNotNull(mDNSNames, "mDNSName");
+ }
+
+ /**
+ * Get the configuration for a vendor.
+ *
+ * @param context Calling context
+ * @param name The name of the config to read
+ *
+ * @return the config for the vendor or null if not found
+ *
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ public static @Nullable VendorConfig getConfig(@NonNull Context context, @NonNull String name)
+ throws IOException, XmlPullParserException {
+ synchronized (sLock) {
+ if (sConfigs == null) {
+ sConfigs = readVendorConfigs(context);
+ }
+
+ return sConfigs.get(name);
+ }
+ }
+
+ /**
+ * Get all known vendor configurations.
+ *
+ * @param context Calling context
+ *
+ * @return The known configurations
+ *
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ public static @NonNull Collection<VendorConfig> getAllConfigs(@NonNull Context context)
+ throws IOException, XmlPullParserException {
+ synchronized (sLock) {
+ if (sConfigs == null) {
+ sConfigs = readVendorConfigs(context);
+ }
+
+ return sConfigs.values();
+ }
+ }
+
+ /**
+ * Read the text from a XML tag.
+ *
+ * @param parser XML parser to read from
+ *
+ * @return The text or "" if no text was found
+ *
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ private static @NonNull String readText(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ String result = "";
+
+ if (parser.next() == XmlPullParser.TEXT) {
+ result = parser.getText();
+ parser.nextTag();
+ }
+
+ return result;
+ }
+
+ /**
+ * Read a tag with a text content from the parser.
+ *
+ * @param parser XML parser to read from
+ * @param tagName The name of the tag to read
+ *
+ * @return The text content of the tag
+ *
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ private static @NonNull String readSimpleTag(@NonNull Context context,
+ @NonNull XmlPullParser parser, @NonNull String tagName, boolean resolveReferences)
+ throws IOException, XmlPullParserException {
+ parser.require(XmlPullParser.START_TAG, null, tagName);
+ String text = readText(parser);
+ parser.require(XmlPullParser.END_TAG, null, tagName);
+
+ if (resolveReferences && text.startsWith("@")) {
+ return context.getResources().getString(
+ context.getResources().getIdentifier(text, null, context.getPackageName()));
+ } else {
+ return text;
+ }
+ }
+
+ /**
+ * Read content of a list of tags.
+ *
+ * @param parser XML parser to read from
+ * @param tagName The name of the list tag
+ * @param subTagName The name of the list-element tags
+ * @param tagReader The {@link TagReader reader} to use to read the tag content
+ * @param <T> The type of the parsed tag content
+ *
+ * @return A list of {@link T}
+ *
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private static @NonNull <T> ArrayList<T> readTagList(@NonNull XmlPullParser parser,
+ @NonNull String tagName, @NonNull String subTagName, @NonNull TagReader<T> tagReader)
+ throws XmlPullParserException, IOException {
+ ArrayList<T> entries = new ArrayList<>();
+
+ parser.require(XmlPullParser.START_TAG, null, tagName);
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (parser.getName().equals(subTagName)) {
+ entries.add(tagReader.readTag(parser, subTagName));
+ } else {
+ throw new XmlPullParserException(
+ "Unexpected subtag of " + tagName + ": " + parser.getName());
+ }
+ }
+
+ return entries;
+ }
+
+ /**
+ * Read the vendor configuration file.
+ *
+ * @param context The content issuing the read
+ *
+ * @return An map pointing from vendor name to config
+ *
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ private static @NonNull ArrayMap<String, VendorConfig> readVendorConfigs(
+ @NonNull final Context context) throws IOException, XmlPullParserException {
+ try (XmlResourceParser parser = context.getResources().getXml(R.xml.vendorconfigs)) {
+ // Skip header
+ int parsingEvent;
+ do {
+ parsingEvent = parser.next();
+ } while (parsingEvent != XmlResourceParser.START_TAG);
+
+ ArrayList<VendorConfig> configs = readTagList(parser, VENDORS_TAG, VENDOR_TAG,
+ new TagReader<VendorConfig>() {
+ public VendorConfig readTag(XmlPullParser parser, String tagName)
+ throws XmlPullParserException, IOException {
+ return readVendorConfig(context, parser, tagName);
+ }
+ });
+
+ ArrayMap<String, VendorConfig> configMap = new ArrayMap<>(configs.size());
+ final int numConfigs = configs.size();
+ for (int i = 0; i < numConfigs; i++) {
+ VendorConfig config = configs.get(i);
+
+ configMap.put(config.name, config);
+ }
+
+ return configMap;
+ }
+ }
+
+ /**
+ * Read a single vendor configuration.
+ *
+ * @param parser XML parser to read from
+ * @param tagName The vendor tag
+ * @param context Calling context
+ *
+ * @return A config
+ *
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private static VendorConfig readVendorConfig(@NonNull final Context context,
+ @NonNull XmlPullParser parser, @NonNull String tagName) throws XmlPullParserException,
+ IOException {
+ parser.require(XmlPullParser.START_TAG, null, tagName);
+
+ String name = null;
+ String packageName = null;
+ List<String> mDNSNames = null;
+
+ while (parser.next() != XmlPullParser.END_TAG) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ String subTagName = parser.getName();
+
+ switch (subTagName) {
+ case NAME_TAG:
+ name = readSimpleTag(context, parser, NAME_TAG, false);
+ break;
+ case PACKAGE_TAG:
+ packageName = readSimpleTag(context, parser, PACKAGE_TAG, true);
+ break;
+ case MDNSNAMES_TAG:
+ mDNSNames = readTagList(parser, MDNSNAMES_TAG, MDNSNAME_TAG,
+ new TagReader<String>() {
+ public String readTag(XmlPullParser parser, String tagName)
+ throws XmlPullParserException, IOException {
+ return readSimpleTag(context, parser, tagName, true);
+ }
+ }
+ );
+ break;
+ default:
+ throw new XmlPullParserException("Unexpected subtag of " + tagName + ": "
+ + subTagName);
+
+ }
+ }
+
+ if (name == null) {
+ throw new XmlPullParserException("name is required");
+ }
+
+ if (packageName == null) {
+ throw new XmlPullParserException("package is required");
+ }
+
+ if (mDNSNames == null) {
+ mDNSNames = Collections.emptyList();
+ }
+
+ // A vendor config should be immutable
+ mDNSNames = Collections.unmodifiableList(mDNSNames);
+
+ return new VendorConfig(name, packageName, mDNSNames);
+ }
+
+ @Override
+ public String toString() {
+ return name + " -> " + packageName + ", " + mDNSNames;
+ }
+
+ /**
+ * Used a a "function pointer" when reading a tag in {@link #readTagList(XmlPullParser, String,
+ * String, TagReader)}.
+ *
+ * @param <T> The type of content to read
+ */
+ private interface TagReader<T> {
+ T readTag(XmlPullParser parser, String tagName) throws XmlPullParserException, IOException;
+ }
+}
diff --git a/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/util/MDNSUtils.java b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/util/MDNSUtils.java
new file mode 100644
index 0000000..0541c35
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/util/MDNSUtils.java
@@ -0,0 +1,98 @@
+/*
+ * (c) Copyright 2016 Mopria Alliance, Inc.
+ * (c) Copyright 2016 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.printservice.recommendation.util;
+
+import android.annotation.NonNull;
+import android.net.nsd.NsdServiceInfo;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utils for dealing with mDNS attributes
+ */
+public class MDNSUtils {
+ public static final String ATTRIBUTE_TY = "ty";
+ public static final String ATTRIBUTE_PRODUCT = "product";
+ public static final String ATTRIBUTE_USB_MFG = "usb_mfg";
+ public static final String ATTRIBUTE_MFG = "mfg";
+
+ /**
+ * Check if the service has any of a set of vendor names.
+ *
+ * @param serviceInfo The service
+ * @param vendorNames The vendors
+ *
+ * @return true iff the has any of the set of vendor names
+ */
+ public static boolean isVendorPrinter(@NonNull NsdServiceInfo serviceInfo,
+ @NonNull Set<String> vendorNames) {
+ for (Map.Entry<String, byte[]> entry : serviceInfo.getAttributes().entrySet()) {
+ // keys are case insensitive
+ String key = entry.getKey().toLowerCase();
+
+ switch (key) {
+ case ATTRIBUTE_TY:
+ case ATTRIBUTE_PRODUCT:
+ case ATTRIBUTE_USB_MFG:
+ case ATTRIBUTE_MFG:
+ if (entry.getValue() != null) {
+ if (containsVendor(new String(entry.getValue(), StandardCharsets.UTF_8),
+ vendorNames)) {
+ return true;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the attribute matches any of the vendor names, ignoring capitalization.
+ *
+ * @param attr The attribute
+ * @param vendorNames The vendor names
+ *
+ * @return true iff the attribute matches any of the vendor names
+ */
+ private static boolean containsVendor(@NonNull String attr, @NonNull Set<String> vendorNames) {
+ for (String name : vendorNames) {
+ if (containsString(attr.toLowerCase(), name.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if a string in another string.
+ *
+ * @param container The string that contains the string
+ * @param contained The string that is contained
+ *
+ * @return true if the string is contained in the other
+ */
+ private static boolean containsString(@NonNull String container, @NonNull String contained) {
+ return container.equalsIgnoreCase(contained) || container.contains(contained + " ");
+ }
+}
diff --git a/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/util/NsdResolveQueue.java b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/util/NsdResolveQueue.java
new file mode 100644
index 0000000..fad50f6
--- /dev/null
+++ b/packages/PrintServiceRecommendationService/src/com/android/printservice/recommendation/util/NsdResolveQueue.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2016 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.printservice.recommendation.util;
+
+import android.annotation.NonNull;
+import android.net.nsd.NsdManager;
+import android.net.nsd.NsdServiceInfo;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.LinkedList;
+
+/**
+ * Nsd resolve requests for the same info cancel each other. Hence this class synchronizes the
+ * resolutions to hide this effect.
+ */
+public class NsdResolveQueue {
+ /** Lock for {@link #sInstance} */
+ private static final Object sLock = new Object();
+
+ /** Instance of this singleton */
+ @GuardedBy("sLock")
+ private static NsdResolveQueue sInstance;
+
+ /** Lock for {@link #mResolveRequests} */
+ private final Object mLock = new Object();
+
+ /** Current set of registered service info resolve attempts */
+ @GuardedBy("mLock")
+ private final LinkedList<NsdResolveRequest> mResolveRequests = new LinkedList<>();
+
+ public static NsdResolveQueue getInstance() {
+ synchronized (sLock) {
+ if (sInstance == null) {
+ sInstance = new NsdResolveQueue();
+ }
+
+ return sInstance;
+ }
+ }
+
+ /**
+ * Container for a request to resolve a serviceInfo.
+ */
+ private static class NsdResolveRequest {
+ final @NonNull NsdManager nsdManager;
+ final @NonNull NsdServiceInfo serviceInfo;
+ final @NonNull NsdManager.ResolveListener listener;
+
+ private NsdResolveRequest(@NonNull NsdManager nsdManager,
+ @NonNull NsdServiceInfo serviceInfo, @NonNull NsdManager.ResolveListener listener) {
+ this.nsdManager = nsdManager;
+ this.serviceInfo = serviceInfo;
+ this.listener = listener;
+ }
+ }
+
+ /**
+ * Resolve a serviceInfo or queue the request if there is a request currently in flight.
+ *
+ * @param nsdManager The nsd manager to use
+ * @param serviceInfo The service info to resolve
+ * @param listener The listener to call back once the info is resolved.
+ */
+ public void resolve(@NonNull NsdManager nsdManager, @NonNull NsdServiceInfo serviceInfo,
+ @NonNull NsdManager.ResolveListener listener) {
+ synchronized (mLock) {
+ mResolveRequests.addLast(new NsdResolveRequest(nsdManager, serviceInfo,
+ new ListenerWrapper(listener)));
+
+ if (mResolveRequests.size() == 1) {
+ resolveNextRequest();
+ }
+ }
+ }
+
+ /**
+ * Wrapper for a {@link NsdManager.ResolveListener}. Calls the listener and then
+ * {@link #resolveNextRequest()}.
+ */
+ private class ListenerWrapper implements NsdManager.ResolveListener {
+ private final @NonNull NsdManager.ResolveListener mListener;
+
+ private ListenerWrapper(@NonNull NsdManager.ResolveListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
+ mListener.onResolveFailed(serviceInfo, errorCode);
+
+ synchronized (mLock) {
+ mResolveRequests.pop();
+ resolveNextRequest();
+ }
+ }
+
+ @Override
+ public void onServiceResolved(NsdServiceInfo serviceInfo) {
+ mListener.onServiceResolved(serviceInfo);
+
+ synchronized (mLock) {
+ mResolveRequests.pop();
+ resolveNextRequest();
+ }
+ }
+ }
+
+ /**
+ * Resolve the next request if there is one.
+ */
+ private void resolveNextRequest() {
+ if (!mResolveRequests.isEmpty()) {
+ NsdResolveRequest request = mResolveRequests.getFirst();
+
+ request.nsdManager.resolveService(request.serviceInfo, request.listener);
+ }
+ }
+
+}
diff --git a/packages/PrintSpooler/res/drawable/ic_download_from_market.xml b/packages/PrintSpooler/res/drawable/ic_download_from_market.xml
new file mode 100644
index 0000000..44a5edf
--- /dev/null
+++ b/packages/PrintSpooler/res/drawable/ic_download_from_market.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="36dp"
+ android:height="36dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:pathData="M40,12h-8L32,8l-4,-4h-8l-4,4v4L8,12c-2.21,0 -3.98,1.79 -3.98,4L4,38c0,2.21 1.79,4 4,4h32c2.21,0 4,-1.79 4,-4L44,16c0,-2.21 -1.79,-4 -4,-4zM20,8h8v4h-8L20,8zM24,38L14,28h6v-8h8v8h6L24,38z"
+ android:fillColor="?android:attr/colorAccent"/>
+</vector>
diff --git a/packages/PrintSpooler/res/layout/print_service_recommendations_list_item.xml b/packages/PrintSpooler/res/layout/print_service_recommendations_list_item.xml
new file mode 100644
index 0000000..86ac26d
--- /dev/null
+++ b/packages/PrintSpooler/res/layout/print_service_recommendations_list_item.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:orientation="horizontal"
+ android:gravity="start|center_vertical">
+
+ <ImageView
+ android:layout_width="36dip"
+ android:layout_height="36dip"
+ android:src="@drawable/ic_download_from_market"
+ android:layout_marginRight="4dip"
+ android:layout_gravity="center_vertical"
+ android:contentDescription="@null" />
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dip">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:singleLine="true"
+ android:ellipsize="end" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/title"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:text="@string/enable_print_service" />
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/packages/PrintSpooler/res/values/strings.xml b/packages/PrintSpooler/res/values/strings.xml
index 2f24d2c..2836adb 100644
--- a/packages/PrintSpooler/res/values/strings.xml
+++ b/packages/PrintSpooler/res/values/strings.xml
@@ -185,6 +185,12 @@
<!-- Label for the list item that links to the list of all print services. [CHAR LIMIT=50] -->
<string name="all_services_title">All services</string>
+ <!-- Subtitle for a print service recommendation. [CHAR LIMIT=50] -->
+ <plurals name="print_services_recommendation_subtitle">
+ <item quantity="one">Install to discover <xliff:g id="count" example="1">%1$s</xliff:g> printer</item>
+ <item quantity="other">Install to discover <xliff:g id="count" example="2">%1$s</xliff:g> printers</item>
+ </plurals>
+
<!-- Notifications -->
<!-- Template for the notification label for a printing print job. [CHAR LIMIT=25] -->
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/AddPrinterActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/AddPrinterActivity.java
index f2b3e6e..42ef10e 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/AddPrinterActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/AddPrinterActivity.java
@@ -30,10 +30,13 @@
import android.net.Uri;
import android.os.Bundle;
import android.print.PrintManager;
+import android.printservice.recommendation.RecommendationInfo;
+import android.print.PrintServiceRecommendationsLoader;
import android.print.PrintServicesLoader;
import android.printservice.PrintServiceInfo;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.view.View;
@@ -45,8 +48,10 @@
import android.widget.TextView;
import com.android.printspooler.R;
+import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
/**
@@ -57,31 +62,38 @@
* when the item is clicked.</li>
* <li>{@link #mDisabledServicesAdapter} for all disabled services. Once clicked the settings page
* for this service is opened.</li>
- * <li>{@link RecommendedServicesAdapter} for a link to all services. If this item is clicked
+ * <li>{@link #mRecommendedServicesAdapter} for a link to all services. If this item is clicked
* the market app is opened to show all print services.</li>
* </ul>
*/
-public class AddPrinterActivity extends ListActivity implements
- LoaderManager.LoaderCallbacks<List<PrintServiceInfo>>,
- AdapterView.OnItemClickListener {
+public class AddPrinterActivity extends ListActivity implements AdapterView.OnItemClickListener {
private static final String LOG_TAG = "AddPrinterActivity";
/** Ids for the loaders */
private static final int LOADER_ID_ENABLED_SERVICES = 1;
private static final int LOADER_ID_DISABLED_SERVICES = 2;
+ private static final int LOADER_ID_RECOMMENDED_SERVICES = 3;
+ private static final int LOADER_ID_ALL_SERVICES = 4;
/**
* The enabled services list. This is filled from the {@link #LOADER_ID_ENABLED_SERVICES}
- * loader in {@link #onLoadFinished}.
+ * loader in {@link PrintServiceInfoLoaderCallbacks#onLoadFinished}.
*/
private EnabledServicesAdapter mEnabledServicesAdapter;
/**
* The disabled services list. This is filled from the {@link #LOADER_ID_DISABLED_SERVICES}
- * loader in {@link #onLoadFinished}.
+ * loader in {@link PrintServiceInfoLoaderCallbacks#onLoadFinished}.
*/
private DisabledServicesAdapter mDisabledServicesAdapter;
+ /**
+ * The recommended services list. This is filled from the
+ * {@link #LOADER_ID_RECOMMENDED_SERVICES} loader in
+ * {@link PrintServicePrintServiceRecommendationLoaderCallbacks#onLoadFinished}.
+ */
+ private RecommendedServicesAdapter mRecommendedServicesAdapter;
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -90,36 +102,116 @@
mEnabledServicesAdapter = new EnabledServicesAdapter();
mDisabledServicesAdapter = new DisabledServicesAdapter();
+ mRecommendedServicesAdapter = new RecommendedServicesAdapter();
ArrayList<ActionAdapter> adapterList = new ArrayList<>(3);
adapterList.add(mEnabledServicesAdapter);
- adapterList.add(new RecommendedServicesAdapter());
+ adapterList.add(mRecommendedServicesAdapter);
adapterList.add(mDisabledServicesAdapter);
setListAdapter(new CombinedAdapter(adapterList));
getListView().setOnItemClickListener(this);
- getLoaderManager().initLoader(LOADER_ID_ENABLED_SERVICES, null, this);
- getLoaderManager().initLoader(LOADER_ID_DISABLED_SERVICES, null, this);
- // TODO: Load recommended services
+ PrintServiceInfoLoaderCallbacks printServiceLoaderCallbacks =
+ new PrintServiceInfoLoaderCallbacks();
+
+ getLoaderManager().initLoader(LOADER_ID_ENABLED_SERVICES, null, printServiceLoaderCallbacks);
+ getLoaderManager().initLoader(LOADER_ID_DISABLED_SERVICES, null, printServiceLoaderCallbacks);
+ getLoaderManager().initLoader(LOADER_ID_RECOMMENDED_SERVICES, null,
+ new PrintServicePrintServiceRecommendationLoaderCallbacks());
+ getLoaderManager().initLoader(LOADER_ID_ALL_SERVICES, null, printServiceLoaderCallbacks);
}
- @Override
- public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
- switch (id) {
- case LOADER_ID_ENABLED_SERVICES:
- return new PrintServicesLoader(
- (PrintManager) getSystemService(Context.PRINT_SERVICE), this,
- PrintManager.ENABLED_SERVICES);
- case LOADER_ID_DISABLED_SERVICES:
- return new PrintServicesLoader(
- (PrintManager) getSystemService(Context.PRINT_SERVICE), this,
- PrintManager.DISABLED_SERVICES);
- // TODO: Load recommended services
- default:
- // not reached
- return null;
+ /**
+ * Callbacks for the loaders operating on list of {@link PrintServiceInfo print service infos}.
+ */
+ private class PrintServiceInfoLoaderCallbacks implements
+ LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
+ @Override
+ public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
+ switch (id) {
+ case LOADER_ID_ENABLED_SERVICES:
+ return new PrintServicesLoader(
+ (PrintManager) getSystemService(Context.PRINT_SERVICE),
+ AddPrinterActivity.this, PrintManager.ENABLED_SERVICES);
+ case LOADER_ID_DISABLED_SERVICES:
+ return new PrintServicesLoader(
+ (PrintManager) getSystemService(Context.PRINT_SERVICE),
+ AddPrinterActivity.this, PrintManager.DISABLED_SERVICES);
+ case LOADER_ID_ALL_SERVICES:
+ return new PrintServicesLoader(
+ (PrintManager) getSystemService(Context.PRINT_SERVICE),
+ AddPrinterActivity.this, PrintManager.ALL_SERVICES);
+ default:
+ // not reached
+ return null;
+ }
+ }
+
+
+ @Override
+ public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
+ List<PrintServiceInfo> data) {
+ switch (loader.getId()) {
+ case LOADER_ID_ENABLED_SERVICES:
+ mEnabledServicesAdapter.updateData(data);
+ break;
+ case LOADER_ID_DISABLED_SERVICES:
+ mDisabledServicesAdapter.updateData(data);
+ break;
+ case LOADER_ID_ALL_SERVICES:
+ mRecommendedServicesAdapter.updateInstalledServices(data);
+ default:
+ // not reached
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
+ if (!isFinishing()) {
+ switch (loader.getId()) {
+ case LOADER_ID_ENABLED_SERVICES:
+ mEnabledServicesAdapter.updateData(null);
+ break;
+ case LOADER_ID_DISABLED_SERVICES:
+ mDisabledServicesAdapter.updateData(null);
+ break;
+ case LOADER_ID_ALL_SERVICES:
+ mRecommendedServicesAdapter.updateInstalledServices(null);
+ break;
+ default:
+ // not reached
+ }
+ }
+ }
+ }
+
+ /**
+ * Callbacks for the loaders operating on list of {@link RecommendationInfo print service
+ * recommendations}.
+ */
+ private class PrintServicePrintServiceRecommendationLoaderCallbacks implements
+ LoaderManager.LoaderCallbacks<List<RecommendationInfo>> {
+ @Override
+ public Loader<List<RecommendationInfo>> onCreateLoader(int id, Bundle args) {
+ return new PrintServiceRecommendationsLoader(
+ (PrintManager) getSystemService(Context.PRINT_SERVICE),
+ AddPrinterActivity.this);
+ }
+
+
+ @Override
+ public void onLoadFinished(Loader<List<RecommendationInfo>> loader,
+ List<RecommendationInfo> data) {
+ mRecommendedServicesAdapter.updateRecommendations(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<List<RecommendationInfo>> loader) {
+ if (!isFinishing()) {
+ mRecommendedServicesAdapter.updateRecommendations(null);
+ }
}
}
@@ -128,39 +220,6 @@
((ActionAdapter) getListAdapter()).performAction(position);
}
- @Override
- public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
- List<PrintServiceInfo> data) {
- switch (loader.getId()) {
- case LOADER_ID_ENABLED_SERVICES:
- mEnabledServicesAdapter.updateData(data);
- break;
- case LOADER_ID_DISABLED_SERVICES:
- mDisabledServicesAdapter.updateData(data);
- break;
- // TODO: Load recommended services
- default:
- // not reached
- }
- }
-
- @Override
- public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
- if (!isFinishing()) {
- switch (loader.getId()) {
- case LOADER_ID_ENABLED_SERVICES:
- mEnabledServicesAdapter.updateData(null);
- break;
- case LOADER_ID_DISABLED_SERVICES:
- mDisabledServicesAdapter.updateData(null);
- break;
- // TODO: Reset recommended services
- default:
- // not reached
- }
- }
- }
-
/**
* Marks an adapter that can can perform an action for a position in it's list.
*/
@@ -490,28 +549,65 @@
* Adapter for the recommended services.
*/
private class RecommendedServicesAdapter extends ActionAdapter {
+ /** Package names of all installed print services */
+ private @NonNull final ArraySet<String> mInstalledServices;
+
+ /** All print service recommendations */
+ private @Nullable List<RecommendationInfo> mRecommendations;
+
+ /**
+ * Sorted print service recommendations for services that are not installed
+ *
+ * @see #filterRecommendations
+ */
+ private @Nullable List<RecommendationInfo> mFilteredRecommendations;
+
+ /**
+ * Create a new adapter.
+ */
+ private RecommendedServicesAdapter() {
+ mInstalledServices = new ArraySet<>();
+ }
+
@Override
public int getCount() {
- return 2;
+ if (mFilteredRecommendations == null) {
+ return 2;
+ } else {
+ return mFilteredRecommendations.size() + 2;
+ }
}
@Override
public int getViewTypeCount() {
- return 2;
+ return 3;
+ }
+
+ /**
+ * @return The position the all services link is at.
+ */
+ private int getAllServicesPos() {
+ return getCount() - 1;
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return 0;
- } else {
+ } else if (getAllServicesPos() == position) {
return 1;
+ } else {
+ return 2;
}
}
@Override
public Object getItem(int position) {
- return null;
+ if (position == 0 || position == getAllServicesPos()) {
+ return null;
+ } else {
+ return mFilteredRecommendations.get(position - 1);
+ }
}
@Override
@@ -531,11 +627,27 @@
.setText(R.string.recommended_services_title);
return convertView;
- }
+ } else if (position == getAllServicesPos()) {
+ if (convertView == null) {
+ convertView = getLayoutInflater().inflate(R.layout.all_print_services_list_item,
+ parent, false);
+ }
+ } else {
+ RecommendationInfo recommendation = (RecommendationInfo) getItem(position);
- if (convertView == null) {
- convertView = getLayoutInflater().inflate(R.layout.all_print_services_list_item,
- parent, false);
+ if (convertView == null) {
+ convertView = getLayoutInflater().inflate(
+ R.layout.print_service_recommendations_list_item, parent, false);
+ }
+
+ ((TextView) convertView.findViewById(R.id.title)).setText(recommendation.getName());
+
+ ((TextView) convertView.findViewById(R.id.subtitle)).setText(getResources()
+ .getQuantityString(R.plurals.print_services_recommendation_subtitle,
+ recommendation.getNumDiscoveredPrinters(),
+ recommendation.getNumDiscoveredPrinters()));
+
+ return convertView;
}
return convertView;
@@ -548,16 +660,107 @@
@Override
public void performAction(@IntRange(from = 0) int position) {
- String searchUri = Settings.Secure
- .getString(getContentResolver(), Settings.Secure.PRINT_SERVICE_SEARCH_URI);
+ if (position == getAllServicesPos()) {
+ String searchUri = Settings.Secure
+ .getString(getContentResolver(), Settings.Secure.PRINT_SERVICE_SEARCH_URI);
- if (searchUri != null) {
+ if (searchUri != null) {
+ try {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)));
+ } catch (ActivityNotFoundException e) {
+ Log.e(LOG_TAG, "Cannot start market", e);
+ }
+ }
+ } else {
+ RecommendationInfo recommendation = (RecommendationInfo) getItem(position);
+
try {
- startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)));
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(
+ R.string.uri_package_details, recommendation.getPackageName()))));
} catch (ActivityNotFoundException e) {
Log.e(LOG_TAG, "Cannot start market", e);
}
}
}
+
+ /**
+ * Filter recommended services.
+ */
+ private void filterRecommendations() {
+ if (mRecommendations == null) {
+ mFilteredRecommendations = null;
+ } else {
+ mFilteredRecommendations = new ArrayList<>();
+
+ // Filter out recommendations for already installed services
+ final int numRecommendations = mRecommendations.size();
+ for (int i = 0; i < numRecommendations; i++) {
+ RecommendationInfo recommendation = mRecommendations.get(i);
+
+ if (!mInstalledServices.contains(recommendation.getPackageName())) {
+ mFilteredRecommendations.add(recommendation);
+ }
+ }
+ }
+
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Update the installed print services.
+ *
+ * @param services The new set of services
+ */
+ public void updateInstalledServices(List<PrintServiceInfo> services) {
+ mInstalledServices.clear();
+
+ final int numServices = services.size();
+ for (int i = 0; i < numServices; i++) {
+ mInstalledServices.add(services.get(i).getComponentName().getPackageName());
+ }
+
+ filterRecommendations();
+ }
+
+ /**
+ * Update the recommended print services.
+ *
+ * @param recommendations The new set of recommendations
+ */
+ public void updateRecommendations(List<RecommendationInfo> recommendations) {
+ if (recommendations != null) {
+ final Collator collator = Collator.getInstance();
+
+ // Sort recommendations (early conditions are more important)
+ // - higher number of discovered printers first
+ // - single vendor services first
+ // - alphabetically
+ Collections.sort(recommendations,
+ new Comparator<RecommendationInfo>() {
+ @Override public int compare(RecommendationInfo o1,
+ RecommendationInfo o2) {
+ if (o1.getNumDiscoveredPrinters() !=
+ o2.getNumDiscoveredPrinters()) {
+ return o2.getNumDiscoveredPrinters() -
+ o1.getNumDiscoveredPrinters();
+ } else if (o1.recommendsMultiVendorService()
+ != o2.recommendsMultiVendorService()) {
+ if (o1.recommendsMultiVendorService()) {
+ return 1;
+ } else {
+ return -1;
+ }
+ } else {
+ return collator.compare(o1.getName().toString(),
+ o2.getName().toString());
+ }
+ }
+ });
+ }
+
+ mRecommendations = recommendations;
+
+ filterRecommendations();
+ }
}
}
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PageAdapter.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PageAdapter.java
index eebb60c..c1a3f86 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/PageAdapter.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PageAdapter.java
@@ -206,6 +206,7 @@
int documentPageCount, MediaSize mediaSize, Margins minMargins) {
boolean documentChanged = false;
boolean updatePreviewAreaAndPageSize = false;
+ boolean clearSelectedPages = false;
// If the app does not tell how many pages are in the document we cannot
// optimize and ask for all pages whose count we get from the renderer.
@@ -225,30 +226,41 @@
}
}
- if (!Arrays.equals(mSelectedPages, selectedPages)) {
- mSelectedPages = selectedPages;
- mSelectedPageCount = PageRangeUtils.getNormalizedPageCount(
- mSelectedPages, documentPageCount);
- setConfirmedPages(mSelectedPages, documentPageCount);
- updatePreviewAreaAndPageSize = true;
- documentChanged = true;
- }
-
if (mDocumentPageCount != documentPageCount) {
mDocumentPageCount = documentPageCount;
documentChanged = true;
+ clearSelectedPages = true;
}
if (mMediaSize == null || !mMediaSize.equals(mediaSize)) {
mMediaSize = mediaSize;
updatePreviewAreaAndPageSize = true;
documentChanged = true;
+
+ clearSelectedPages = true;
}
if (mMinMargins == null || !mMinMargins.equals(minMargins)) {
mMinMargins = minMargins;
updatePreviewAreaAndPageSize = true;
documentChanged = true;
+
+ clearSelectedPages = true;
+ }
+
+ if (clearSelectedPages) {
+ mSelectedPages = PageRange.ALL_PAGES_ARRAY;
+ mSelectedPageCount = documentPageCount;
+ setConfirmedPages(mSelectedPages, documentPageCount);
+ updatePreviewAreaAndPageSize = true;
+ documentChanged = true;
+ } else if (!Arrays.equals(mSelectedPages, selectedPages)) {
+ mSelectedPages = selectedPages;
+ mSelectedPageCount = PageRangeUtils.getNormalizedPageCount(
+ mSelectedPages, documentPageCount);
+ setConfirmedPages(mSelectedPages, documentPageCount);
+ updatePreviewAreaAndPageSize = true;
+ documentChanged = true;
}
// If *all pages* is selected we need to convert that to absolute
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
index a24d664..dba6a8b 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
@@ -128,8 +128,6 @@
private static final boolean DEBUG = false;
- public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID";
-
private static final String FRAGMENT_TAG = "FRAGMENT_TAG";
private static final String HAS_PRINTED_PREF = "has_printed";
@@ -176,8 +174,6 @@
"[\\s]*[0-9]+[\\-]?[\\s]*[0-9]*[\\s]*?(([,])"
+ "[\\s]*[0-9]+[\\s]*[\\-]?[\\s]*[0-9]*[\\s]*|[\\s]*)+");
- public static final PageRange[] ALL_PAGES_ARRAY = new PageRange[]{PageRange.ALL_PAGES};
-
private boolean mIsOptionsUiBound = false;
private final PrinterAvailabilityDetector mPrinterAvailabilityDetector =
@@ -597,14 +593,6 @@
@Override
public void onOptionsClosed() {
- PageRange[] selectedPages = computeSelectedPages();
- if (!Arrays.equals(mSelectedPages, selectedPages)) {
- mSelectedPages = selectedPages;
-
- // Update preview.
- updatePrintPreviewController(false);
- }
-
// Make sure the IME is not on the way of preview as
// the user may have used it to type copies or range.
InputMethodManager imm = getSystemService(InputMethodManager.class);
@@ -717,21 +705,17 @@
private void onSelectPrinterActivityResult(int resultCode, Intent data) {
if (resultCode == RESULT_OK && data != null) {
- PrinterId printerId = data.getParcelableExtra(INTENT_EXTRA_PRINTER_ID);
- if (printerId != null) {
- mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerId);
- final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
- if (index != AdapterView.INVALID_POSITION) {
- mDestinationSpinner.setSelection(index);
- return;
- }
+ PrinterInfo printerInfo = data.getParcelableExtra(
+ SelectPrinterActivity.INTENT_EXTRA_PRINTER);
+ if (printerInfo != null) {
+ mCurrentPrinter = printerInfo;
+ mDestinationSpinnerAdapter.ensurePrinterInVisibleAdapterPosition(printerInfo);
}
}
if (mCurrentPrinter != null) {
- PrinterId printerId = mCurrentPrinter.getId();
- final int index = mDestinationSpinnerAdapter.getPrinterIndex(printerId);
- mDestinationSpinner.setSelection(index);
+ // Trigger PrintersObserver.onChanged() to adjust selection back to current printer
+ mDestinationSpinnerAdapter.notifyDataSetChanged();
}
}
@@ -950,7 +934,7 @@
mSelectedPages = selectedPages;
mPrintJob.setPages(selectedPages);
- if (Arrays.equals(selectedPages, ALL_PAGES_ARRAY)) {
+ if (Arrays.equals(selectedPages, PageRange.ALL_PAGES_ARRAY)) {
if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
mRangeOptionsSpinner.setSelection(0);
mPageRangeEditText.setText("");
@@ -1049,7 +1033,22 @@
}
}
+ /**
+ * Clear the selected page range and update the preview if needed.
+ */
+ private void clearPageRanges() {
+ mRangeOptionsSpinner.setSelection(0);
+ mPageRangeEditText.setError(null);
+ mPageRangeEditText.setText("");
+ mSelectedPages = PageRange.ALL_PAGES_ARRAY;
+
+ if (!Arrays.equals(mSelectedPages, mPrintPreviewController.getSelectedPages())) {
+ updatePrintPreviewController(false);
+ }
+ }
+
private void updatePrintAttributesFromCapabilities(PrinterCapabilitiesInfo capabilities) {
+ boolean clearRanges = false;
PrintAttributes defaults = capabilities.getDefaults();
// Sort the media sizes based on the current locale.
@@ -1061,6 +1060,7 @@
// Media size.
MediaSize currMediaSize = attributes.getMediaSize();
if (currMediaSize == null) {
+ clearRanges = true;
attributes.setMediaSize(defaults.getMediaSize());
} else {
MediaSize newMediaSize = null;
@@ -1079,6 +1079,7 @@
}
// If we did not find the current media size fall back to default.
if (newMediaSize == null) {
+ clearRanges = true;
newMediaSize = defaults.getMediaSize();
}
@@ -1110,7 +1111,14 @@
}
// Margins.
+ if (!Objects.equals(attributes.getMinMargins(), defaults.getMinMargins())) {
+ clearRanges = true;
+ }
attributes.setMinMargins(defaults.getMinMargins());
+
+ if (clearRanges) {
+ clearPageRanges();
+ }
}
private boolean updateDocument(boolean clearLastError) {
@@ -1262,6 +1270,7 @@
// Page range
mPageRangeTitle = (TextView) findViewById(R.id.page_range_title);
mPageRangeEditText = (EditText) findViewById(R.id.page_range_edittext);
+ mPageRangeEditText.setVisibility(View.INVISIBLE);
mPageRangeEditText.setOnFocusChangeListener(mSelectAllOnFocusListener);
mPageRangeEditText.addTextChangedListener(new RangeTextWatcher());
@@ -1753,36 +1762,38 @@
// Range options
PrintDocumentInfo info = mPrintedDocument.getDocumentInfo().info;
final int pageCount = getAdjustedPageCount(info);
- if (info != null && pageCount > 0) {
- if (pageCount == 1) {
- mRangeOptionsSpinner.setEnabled(false);
- } else {
- mRangeOptionsSpinner.setEnabled(true);
- if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
- if (!mPageRangeEditText.isEnabled()) {
- mPageRangeEditText.setEnabled(true);
- mPageRangeEditText.setVisibility(View.VISIBLE);
- mPageRangeTitle.setVisibility(View.VISIBLE);
- mPageRangeEditText.requestFocus();
- InputMethodManager imm = (InputMethodManager)
- getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.showSoftInput(mPageRangeEditText, 0);
- }
+ if (pageCount > 0) {
+ if (info != null) {
+ if (pageCount == 1) {
+ mRangeOptionsSpinner.setEnabled(false);
} else {
- mPageRangeEditText.setEnabled(false);
- mPageRangeEditText.setVisibility(View.INVISIBLE);
- mPageRangeTitle.setVisibility(View.INVISIBLE);
+ mRangeOptionsSpinner.setEnabled(true);
+ if (mRangeOptionsSpinner.getSelectedItemPosition() > 0) {
+ if (!mPageRangeEditText.isEnabled()) {
+ mPageRangeEditText.setEnabled(true);
+ mPageRangeEditText.setVisibility(View.VISIBLE);
+ mPageRangeTitle.setVisibility(View.VISIBLE);
+ mPageRangeEditText.requestFocus();
+ InputMethodManager imm = (InputMethodManager)
+ getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(mPageRangeEditText, 0);
+ }
+ } else {
+ mPageRangeEditText.setEnabled(false);
+ mPageRangeEditText.setVisibility(View.INVISIBLE);
+ mPageRangeTitle.setVisibility(View.INVISIBLE);
+ }
}
+ } else {
+ if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
+ mRangeOptionsSpinner.setSelection(0);
+ mPageRangeEditText.setText("");
+ }
+ mRangeOptionsSpinner.setEnabled(false);
+ mPageRangeEditText.setEnabled(false);
+ mPageRangeEditText.setVisibility(View.INVISIBLE);
+ mPageRangeTitle.setVisibility(View.INVISIBLE);
}
- } else {
- if (mRangeOptionsSpinner.getSelectedItemPosition() != 0) {
- mRangeOptionsSpinner.setSelection(0);
- mPageRangeEditText.setText("");
- }
- mRangeOptionsSpinner.setEnabled(false);
- mPageRangeEditText.setEnabled(false);
- mPageRangeEditText.setVisibility(View.INVISIBLE);
- mPageRangeTitle.setVisibility(View.INVISIBLE);
}
final int newPageCount = getAdjustedPageCount(info);
@@ -1933,7 +1944,7 @@
return PageRangeUtils.normalize(pageRangesArray);
}
- return ALL_PAGES_ARRAY;
+ return PageRange.ALL_PAGES_ARRAY;
}
private int getAdjustedPageCount(PrintDocumentInfo info) {
@@ -2229,23 +2240,36 @@
return AdapterView.INVALID_POSITION;
}
- public void ensurePrinterInVisibleAdapterPosition(PrinterId printerId) {
+ public void ensurePrinterInVisibleAdapterPosition(PrinterInfo printer) {
final int printerCount = mPrinterHolders.size();
+ boolean isKnownPrinter = false;
for (int i = 0; i < printerCount; i++) {
PrinterHolder printerHolder = mPrinterHolders.get(i);
- if (printerHolder.printer.getId().equals(printerId)) {
+
+ if (printerHolder.printer.getId().equals(printer.getId())) {
+ isKnownPrinter = true;
+
// If already in the list - do nothing.
if (i < getCount() - 2) {
- return;
+ break;
}
// Else replace the last one (two items are not printers).
final int lastPrinterIndex = getCount() - 3;
mPrinterHolders.set(i, mPrinterHolders.get(lastPrinterIndex));
mPrinterHolders.set(lastPrinterIndex, printerHolder);
- notifyDataSetChanged();
- return;
+ break;
}
}
+
+ if (!isKnownPrinter) {
+ PrinterHolder printerHolder = new PrinterHolder(printer);
+ printerHolder.removed = true;
+
+ mPrinterHolders.add(Math.max(0, getCount() - 3), printerHolder);
+ }
+
+ // Force reload to adjust selection in PrintersObserver.onChanged()
+ notifyDataSetChanged();
}
@Override
@@ -2428,8 +2452,7 @@
List<PrinterHolder> newPrinterHolders = new ArrayList<>();
// Update printers we already have which are either updated or removed.
- // We do not remove printers if the currently selected printer is removed
- // to prevent the user printing to a wrong printer.
+ // We do not remove the currently selected printer.
final int oldPrinterCount = mPrinterHolders.size();
for (int i = 0; i < oldPrinterCount; i++) {
PrinterHolder printerHolder = mPrinterHolders.get(i);
@@ -2438,10 +2461,11 @@
if (updatedPrinter != null) {
printerHolder.printer = updatedPrinter;
printerHolder.removed = false;
- } else {
+ newPrinterHolders.add(printerHolder);
+ } else if (mCurrentPrinter != null && mCurrentPrinter.getId().equals(oldPrinterId)){
printerHolder.removed = true;
+ newPrinterHolders.add(printerHolder);
}
- newPrinterHolders.add(printerHolder);
}
// Add the rest of the new printers, i.e. what is left.
@@ -2473,14 +2497,25 @@
return null;
}
- public void pruneRemovedPrinters() {
+ /**
+ * Remove a printer from the holders if it is marked as removed.
+ *
+ * @param printerId the id of the printer to remove.
+ *
+ * @return true iff the printer was removed.
+ */
+ public boolean pruneRemovedPrinter(PrinterId printerId) {
final int holderCounts = mPrinterHolders.size();
for (int i = holderCounts - 1; i >= 0; i--) {
PrinterHolder printerHolder = mPrinterHolders.get(i);
- if (printerHolder.removed) {
+
+ if (printerHolder.printer.getId().equals(printerId) && printerHolder.removed) {
mPrinterHolders.remove(i);
+ return true;
}
}
+
+ return false;
}
private void addPrinters(List<PrinterHolder> list, Collection<PrinterInfo> printers) {
@@ -2525,17 +2560,17 @@
PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
oldPrinterState.getId());
- if (printerHolder == null) {
- return;
- }
PrinterInfo newPrinterState = printerHolder.printer;
- if (!printerHolder.removed) {
- mDestinationSpinnerAdapter.pruneRemovedPrinters();
- } else {
+ if (printerHolder.removed) {
onPrinterUnavailable(newPrinterState);
}
+ if (mDestinationSpinner.getSelectedItem() != printerHolder) {
+ mDestinationSpinner.setSelection(
+ mDestinationSpinnerAdapter.getPrinterIndex(newPrinterState.getId()));
+ }
+
if (oldPrinterState.equals(newPrinterState)) {
return;
}
@@ -2607,6 +2642,8 @@
private final class MyOnItemSelectedListener implements AdapterView.OnItemSelectedListener {
@Override
public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) {
+ boolean clearRanges = false;
+
if (spinner == mDestinationSpinner) {
if (position == AdapterView.INVALID_POSITION) {
return;
@@ -2625,13 +2662,28 @@
return;
}
+ PrinterId oldId = null;
+ if (mCurrentPrinter != null) {
+ oldId = mCurrentPrinter.getId();
+ }
+
mCurrentPrinter = currentPrinter;
+ if (oldId != null) {
+ boolean printerRemoved = mDestinationSpinnerAdapter.pruneRemovedPrinter(oldId);
+
+ if (printerRemoved) {
+ // Trigger PrinterObserver.onChanged to adjust selection. This will call
+ // this function again.
+ mDestinationSpinnerAdapter.notifyDataSetChanged();
+ return;
+ }
+ }
+
PrinterHolder printerHolder = mDestinationSpinnerAdapter.getPrinterHolder(
currentPrinter.getId());
if (!printerHolder.removed) {
setState(STATE_CONFIGURING);
- mDestinationSpinnerAdapter.pruneRemovedPrinters();
ensurePreviewUiShown();
}
@@ -2653,10 +2705,17 @@
} else if (spinner == mMediaSizeSpinner) {
SpinnerItem<MediaSize> mediaItem = mMediaSizeSpinnerAdapter.getItem(position);
PrintAttributes attributes = mPrintJob.getAttributes();
+
+ MediaSize newMediaSize;
if (mOrientationSpinner.getSelectedItemPosition() == 0) {
- attributes.setMediaSize(mediaItem.value.asPortrait());
+ newMediaSize = mediaItem.value.asPortrait();
} else {
- attributes.setMediaSize(mediaItem.value.asLandscape());
+ newMediaSize = mediaItem.value.asLandscape();
+ }
+
+ if (newMediaSize != attributes.getMediaSize()) {
+ clearRanges = true;
+ attributes.setMediaSize(newMediaSize);
}
} else if (spinner == mColorModeSpinner) {
SpinnerItem<Integer> colorModeItem = mColorModeSpinnerAdapter.getItem(position);
@@ -2668,25 +2727,35 @@
SpinnerItem<Integer> orientationItem = mOrientationSpinnerAdapter.getItem(position);
PrintAttributes attributes = mPrintJob.getAttributes();
if (mMediaSizeSpinner.getSelectedItem() != null) {
- if (orientationItem.value == ORIENTATION_PORTRAIT) {
- attributes.copyFrom(attributes.asPortrait());
- } else {
- attributes.copyFrom(attributes.asLandscape());
+ boolean isPortrait = attributes.isPortrait();
+
+ if (isPortrait != (orientationItem.value == ORIENTATION_PORTRAIT)) {
+ clearRanges = true;
+ if (orientationItem.value == ORIENTATION_PORTRAIT) {
+ attributes.copyFrom(attributes.asPortrait());
+ } else {
+ attributes.copyFrom(attributes.asLandscape());
+ }
}
}
} else if (spinner == mRangeOptionsSpinner) {
if (mRangeOptionsSpinner.getSelectedItemPosition() == 0) {
+ clearRanges = true;
mPageRangeEditText.setText("");
} else if (TextUtils.isEmpty(mPageRangeEditText.getText())) {
mPageRangeEditText.setError("");
}
}
- if (canUpdateDocument()) {
- updateDocument(false);
+ if (clearRanges) {
+ clearPageRanges();
}
updateOptionsUi();
+
+ if (canUpdateDocument()) {
+ updateDocument(false);
+ }
}
@Override
@@ -2702,6 +2771,16 @@
if (!TextUtils.isEmpty(editText.getText())) {
editText.setSelection(editText.getText().length());
}
+
+ if (view == mPageRangeEditText && !hasFocus) {
+ PageRange[] selectedPages = computeSelectedPages();
+ if (selectedPages != null && !Arrays.equals(mSelectedPages, selectedPages)) {
+ mSelectedPages = selectedPages;
+
+ // Update preview.
+ updatePrintPreviewController(false);
+ }
+ }
}
}
@@ -2719,19 +2798,22 @@
@Override
public void afterTextChanged(Editable editable) {
final boolean hadErrors = hasErrors();
-
String text = editable.toString();
if (TextUtils.isEmpty(text)) {
- mPageRangeEditText.setError("");
- updateOptionsUi();
+ if (mPageRangeEditText.getError() == null) {
+ mPageRangeEditText.setError("");
+ updateOptionsUi();
+ }
return;
}
String escapedText = PATTERN_ESCAPE_SPECIAL_CHARS.matcher(text).replaceAll("////");
if (!PATTERN_PAGE_RANGE.matcher(escapedText).matches()) {
- mPageRangeEditText.setError("");
- updateOptionsUi();
+ if (mPageRangeEditText.getError() == null) {
+ mPageRangeEditText.setError("");
+ updateOptionsUi();
+ }
return;
}
@@ -2747,8 +2829,10 @@
}
final int pageIndex = Integer.parseInt(numericString);
if (pageIndex < 1 || pageIndex > pageCount) {
- mPageRangeEditText.setError("");
- updateOptionsUi();
+ if (mPageRangeEditText.getError() == null) {
+ mPageRangeEditText.setError("");
+ updateOptionsUi();
+ }
return;
}
}
@@ -2757,13 +2841,14 @@
// greater than the to page. When computing the requested pages
// we just swap them if necessary.
- mPageRangeEditText.setError(null);
- mPrintButton.setEnabled(true);
- updateOptionsUi();
-
- if (hadErrors && !hasErrors()) {
+ if (mPageRangeEditText.getError() != null) {
+ mPageRangeEditText.setError(null);
updateOptionsUi();
}
+
+ if (hadErrors && canUpdateDocument()) {
+ updateDocument(false);
+ }
}
}
@@ -2783,8 +2868,10 @@
final boolean hadErrors = hasErrors();
if (editable.length() == 0) {
- mCopiesEditText.setError("");
- updateOptionsUi();
+ if (mCopiesEditText.getError() == null) {
+ mCopiesEditText.setError("");
+ updateOptionsUi();
+ }
return;
}
@@ -2796,16 +2883,19 @@
}
if (copies < MIN_COPIES) {
- mCopiesEditText.setError("");
- updateOptionsUi();
+ if (mCopiesEditText.getError() == null) {
+ mCopiesEditText.setError("");
+ updateOptionsUi();
+ }
return;
}
mPrintJob.setCopies(copies);
- mCopiesEditText.setError(null);
-
- updateOptionsUi();
+ if (mCopiesEditText.getError() != null) {
+ mCopiesEditText.setError(null);
+ updateOptionsUi();
+ }
if (hadErrors && canUpdateDocument()) {
updateDocument(false);
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java
index e53a522..cee16c8 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/SelectPrinterActivity.java
@@ -73,8 +73,9 @@
private static final int LOADER_ID_PRINT_REGISTRY_INT = 2;
private static final int LOADER_ID_ENABLED_PRINT_SERVICES = 3;
- public static final String INTENT_EXTRA_PRINTER_ID = "INTENT_EXTRA_PRINTER_ID";
+ public static final String INTENT_EXTRA_PRINTER = "INTENT_EXTRA_PRINTER";
+ private static final String EXTRA_PRINTER = "EXTRA_PRINTER";
private static final String EXTRA_PRINTER_ID = "EXTRA_PRINTER_ID";
private static final String KEY_NOT_FIRST_CREATE = "KEY_NOT_FIRST_CREATE";
@@ -134,7 +135,7 @@
if (printer == null) {
startAddPrinterActivity();
} else {
- onPrinterSelected(printer.getId());
+ onPrinterSelected(printer);
}
}
});
@@ -244,7 +245,7 @@
MenuItem selectItem = menu.add(Menu.NONE, R.string.print_select_printer,
Menu.NONE, R.string.print_select_printer);
Intent intent = new Intent();
- intent.putExtra(EXTRA_PRINTER_ID, printer.getId());
+ intent.putExtra(EXTRA_PRINTER, printer);
selectItem.setIntent(intent);
}
@@ -263,8 +264,8 @@
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.string.print_select_printer: {
- PrinterId printerId = item.getIntent().getParcelableExtra(EXTRA_PRINTER_ID);
- onPrinterSelected(printerId);
+ PrinterInfo printer = item.getIntent().getParcelableExtra(EXTRA_PRINTER);
+ onPrinterSelected(printer);
} return true;
case R.string.print_forget_printer: {
@@ -302,9 +303,9 @@
super.onStop();
}
- private void onPrinterSelected(PrinterId printerId) {
+ private void onPrinterSelected(PrinterInfo printer) {
Intent intent = new Intent();
- intent.putExtra(INTENT_EXTRA_PRINTER_ID, printerId);
+ intent.putExtra(INTENT_EXTRA_PRINTER, printer);
setResult(RESULT_OK, intent);
finish();
}
diff --git a/packages/SettingsLib/res/values/config.xml b/packages/SettingsLib/res/values/config.xml
new file mode 100755
index 0000000..299a5b7
--- /dev/null
+++ b/packages/SettingsLib/res/values/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2016, 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>
+ <!-- Configuration for automotive -->
+ <bool name="enable_pbap_pce_profile">false</bool>
+</resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java
index 1f1a9b8..1135200 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java
@@ -78,7 +78,8 @@
int deviceOwnerUserId = dpm.getDeviceOwnerUserId();
boolean enforcedByDeviceOwner = false;
if (deviceOwner != null && deviceOwnerUserId != UserHandle.USER_NULL) {
- Bundle enforcedRestrictions = dpm.getUserRestrictions(deviceOwner, deviceOwnerUserId);
+ Bundle enforcedRestrictions =
+ dpm.getUserRestrictionsForUser(deviceOwner, deviceOwnerUserId);
if (enforcedRestrictions != null
&& enforcedRestrictions.getBoolean(userRestriction, false)) {
enforcedByDeviceOwner = true;
@@ -90,7 +91,8 @@
if (userId != UserHandle.USER_NULL) {
profileOwner = dpm.getProfileOwnerAsUser(userId);
if (profileOwner != null) {
- Bundle enforcedRestrictions = dpm.getUserRestrictions(profileOwner, userId);
+ Bundle enforcedRestrictions =
+ dpm.getUserRestrictionsForUser(profileOwner, userId);
if (enforcedRestrictions != null
&& enforcedRestrictions.getBoolean(userRestriction, false)) {
enforcedByProfileOwner = true;
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
index 6226b23..6052ccd 100755
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java
@@ -24,12 +24,14 @@
import android.bluetooth.BluetoothMap;
import android.bluetooth.BluetoothInputDevice;
import android.bluetooth.BluetoothPan;
+import android.bluetooth.BluetoothPbapClient;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.content.Context;
import android.content.Intent;
import android.os.ParcelUuid;
import android.util.Log;
+import com.android.settingslib.R;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -82,7 +84,9 @@
private final HidProfile mHidProfile;
private OppProfile mOppProfile;
private final PanProfile mPanProfile;
+ private PbapClientProfile mPbapClientProfile;
private final PbapServerProfile mPbapProfile;
+ private final boolean mUsePbapPce;
/**
* Mapping from profile name, e.g. "HEADSET" to profile object.
@@ -99,6 +103,7 @@
mLocalAdapter = adapter;
mDeviceManager = deviceManager;
mEventManager = eventManager;
+ mUsePbapPce = mContext.getResources().getBoolean(R.bool.enable_pbap_pce_profile);
// pass this reference to adapter and event manager (circular dependency)
mLocalAdapter.setProfileManager(this);
mEventManager.setProfileManager(this);
@@ -205,9 +210,24 @@
} else if (mOppProfile != null) {
Log.w(TAG, "Warning: OPP profile was previously added but the UUID is now missing.");
}
+
+ //PBAP Client
+ if (mUsePbapPce) {
+ if (mPbapClientProfile == null) {
+ if(DEBUG) Log.d(TAG, "Adding local PBAP Client profile");
+ mPbapClientProfile = new PbapClientProfile(mContext, mLocalAdapter, mDeviceManager,
+ this);
+ addProfile(mPbapClientProfile, PbapClientProfile.NAME,
+ BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED);
+ }
+ } else if (mPbapClientProfile != null) {
+ Log.w(TAG,
+ "Warning: PBAP Client profile was previously added but the UUID is now missing.");
+ }
+
mEventManager.registerProfileIntentReceiver();
- // There is no local SDP record for HID and Settings app doesn't control PBAP
+ // There is no local SDP record for HID and Settings app doesn't control PBAP Server.
}
private final Collection<ServiceListener> mServiceListeners =
@@ -351,6 +371,10 @@
}
}
+ public PbapClientProfile getPbapClientProfile() {
+ return mPbapClientProfile;
+ }
+
public PbapServerProfile getPbapProfile(){
return mPbapProfile;
}
@@ -430,6 +454,12 @@
removedProfiles.remove(mMapProfile);
mMapProfile.setPreferred(device, true);
}
- }
+ if (mUsePbapPce) {
+ profiles.add(mPbapClientProfile);
+ removedProfiles.remove(mPbapClientProfile);
+ profiles.remove(mPbapProfile);
+ removedProfiles.add(mPbapProfile);
+ }
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/PbapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/PbapClientProfile.java
new file mode 100755
index 0000000..aa95be2
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/PbapClientProfile.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2016 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.settingslib.bluetooth;
+
+import android.bluetooth.BluetoothPbapClient;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothUuid;
+import android.content.Context;
+import android.os.ParcelUuid;
+import android.util.Log;
+
+import com.android.settingslib.R;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+final class PbapClientProfile implements LocalBluetoothProfile {
+ private static final String TAG = "PbapClientProfile";
+ private static boolean V = false;
+
+ private BluetoothPbapClient mService;
+ private boolean mIsProfileReady;
+
+ private final LocalBluetoothAdapter mLocalAdapter;
+ private final CachedBluetoothDeviceManager mDeviceManager;
+
+ static final ParcelUuid[] SRC_UUIDS = {
+ BluetoothUuid.PBAP_PSE,
+ };
+
+ static final String NAME = "PbapClient";
+ private final LocalBluetoothProfileManager mProfileManager;
+
+ // Order of this profile in device profiles list
+ private static final int ORDINAL = 6;
+
+ // These callbacks run on the main thread.
+ private final class PbapClientServiceListener
+ implements BluetoothProfile.ServiceListener {
+
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ if (V) {
+ Log.d(TAG,"Bluetooth service connected");
+ }
+ mService = (BluetoothPbapClient) proxy;
+ // We just bound to the service, so refresh the UI for any connected PBAP devices.
+ List<BluetoothDevice> deviceList = mService.getConnectedDevices();
+ while (!deviceList.isEmpty()) {
+ BluetoothDevice nextDevice = deviceList.remove(0);
+ CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
+ // we may add a new device here, but generally this should not happen
+ if (device == null) {
+ Log.w(TAG, "PbapClientProfile found new device: " + nextDevice);
+ device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice);
+ }
+ device.onProfileStateChanged(PbapClientProfile.this, BluetoothProfile.STATE_CONNECTED);
+ device.refresh();
+ }
+ mIsProfileReady = true;
+ }
+
+ public void onServiceDisconnected(int profile) {
+ if (V) {
+ Log.d(TAG,"Bluetooth service disconnected");
+ }
+ mIsProfileReady = false;
+ }
+ }
+
+ private void refreshProfiles() {
+ Collection<CachedBluetoothDevice> cachedDevices = mDeviceManager.getCachedDevicesCopy();
+ for (CachedBluetoothDevice device : cachedDevices) {
+ device.onUuidChanged();
+ }
+ }
+
+ public boolean pbapClientExists() {
+ return (mService != null);
+ }
+
+ public boolean isProfileReady() {
+ return mIsProfileReady;
+ }
+
+ PbapClientProfile(Context context, LocalBluetoothAdapter adapter,
+ CachedBluetoothDeviceManager deviceManager,
+ LocalBluetoothProfileManager profileManager) {
+ mLocalAdapter = adapter;
+ mDeviceManager = deviceManager;
+ mProfileManager = profileManager;
+ mLocalAdapter.getProfileProxy(context, new PbapClientServiceListener(),
+ BluetoothProfile.PBAP_CLIENT);
+ }
+
+ public boolean isConnectable() {
+ return true;
+ }
+
+ public boolean isAutoConnectable() {
+ return true;
+ }
+
+ public List<BluetoothDevice> getConnectedDevices() {
+ if (mService == null) {
+ return new ArrayList<BluetoothDevice>(0);
+ }
+ return mService.getDevicesMatchingConnectionStates(
+ new int[] {BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.STATE_CONNECTING,
+ BluetoothProfile.STATE_DISCONNECTING});
+ }
+
+ public boolean connect(BluetoothDevice device) {
+ if (V) {
+ Log.d(TAG,"PBAPClientProfile got connect request");
+ }
+ if (mService == null) {
+ return false;
+ }
+ List<BluetoothDevice> srcs = getConnectedDevices();
+ if (srcs != null) {
+ for (BluetoothDevice src : srcs) {
+ if (src.equals(device)) {
+ // Connect to same device, Ignore it
+ Log.d(TAG,"Ignoring Connect");
+ return true;
+ }
+ }
+ mService.disconnect(device);
+ }
+ Log.d(TAG,"PBAPClientProfile attempting to connect to " + device.getAddress());
+
+ return mService.connect(device);
+ }
+
+ public boolean disconnect(BluetoothDevice device) {
+ if (V) {
+ Log.d(TAG,"PBAPClientProfile got disconnect request");
+ }
+ if (mService == null) {
+ return false;
+ }
+ return mService.disconnect(device);
+ }
+
+ public int getConnectionStatus(BluetoothDevice device) {
+ if (mService == null) {
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ return mService.getConnectionState(device);
+ }
+
+ public boolean isPreferred(BluetoothDevice device) {
+ if (mService == null) {
+ return false;
+ }
+ return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
+ }
+
+ public int getPreferred(BluetoothDevice device) {
+ if (mService == null) {
+ return BluetoothProfile.PRIORITY_OFF;
+ }
+ return mService.getPriority(device);
+ }
+
+ public void setPreferred(BluetoothDevice device, boolean preferred) {
+ if (mService == null) {
+ return;
+ }
+ if (preferred) {
+ if (mService.getPriority(device) < BluetoothProfile.PRIORITY_ON) {
+ mService.setPriority(device, BluetoothProfile.PRIORITY_ON);
+ }
+ } else {
+ mService.setPriority(device, BluetoothProfile.PRIORITY_OFF);
+ }
+ }
+
+ public String toString() {
+ return NAME;
+ }
+
+ public int getOrdinal() {
+ return ORDINAL;
+ }
+
+ public int getNameResource(BluetoothDevice device) {
+ // we need to have same string in UI as the server side.
+ return R.string.bluetooth_profile_pbap;
+ }
+
+ public int getSummaryResourceForDevice(BluetoothDevice device) {
+ return R.string.bluetooth_profile_pbap_summary;
+ }
+
+ public int getDrawableResource(BluetoothClass btClass) {
+ return R.drawable.ic_bt_cellphone;
+ }
+
+ protected void finalize() {
+ if (V) {
+ Log.d(TAG, "finalize()");
+ }
+ if (mService != null) {
+ try {
+ BluetoothAdapter.getDefaultAdapter().closeProfileProxy(
+ BluetoothProfile.PBAP_CLIENT,mService);
+ mService = null;
+ } catch (Throwable t) {
+ Log.w(TAG, "Error cleaning up PBAP Client proxy", t);
+ }
+ }
+ }
+}
diff --git a/packages/Shell/src/com/android/shell/BugreportStorageProvider.java b/packages/Shell/src/com/android/shell/BugreportStorageProvider.java
index 49759c5..814aa8c 100644
--- a/packages/Shell/src/com/android/shell/BugreportStorageProvider.java
+++ b/packages/Shell/src/com/android/shell/BugreportStorageProvider.java
@@ -56,7 +56,7 @@
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
final RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, DOC_ID_ROOT);
- row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY);
+ row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED);
row.add(Root.COLUMN_ICON, android.R.mipmap.sym_def_app_icon);
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.bugreport_storage_title));
row.add(Root.COLUMN_DOCUMENT_ID, DOC_ID_ROOT);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
index 83736ad..58d7c81 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSAnimator.java
@@ -64,6 +64,7 @@
private boolean mAllowFancy;
private boolean mFullRows;
private int mNumQuickTiles;
+ private float mLastPosition;
public QSAnimator(QSContainer container, QuickQSPanel quickPanel, QSPanel panel) {
mQsContainer = container;
@@ -260,6 +261,7 @@
if (mOnKeyguard) {
return;
}
+ mLastPosition = position;
if (mOnFirstPage && mAllowFancy) {
mQuickQsPanel.setAlpha(1);
mFirstPageAnimator.setPosition(position);
@@ -300,7 +302,7 @@
private void clearAnimationState() {
final int N = mAllViews.size();
mQuickQsPanel.setAlpha(0);
- mQuickQsPanel.setVisibility(View.INVISIBLE);
+ mQuickQsPanel.setVisibility(View.VISIBLE);
for (int i = 0; i < N; i++) {
View v = mAllViews.get(i);
v.setAlpha(1);
@@ -338,6 +340,7 @@
@Override
public void run() {
updateAnimators();
+ setPosition(mLastPosition);
}
};
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
index 8777a91..3e32905 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
@@ -24,8 +24,6 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
-import android.os.UserHandle;
-import android.os.UserManager;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
@@ -85,8 +83,6 @@
abstract protected void handleClick();
abstract protected void handleUpdateState(TState state, Object arg);
- private UserManager mUserManager;
-
/**
* Declare the category of this tile.
*
@@ -99,7 +95,6 @@
mHost = host;
mContext = host.getContext();
mHandler = new H(host.getLooper());
- mUserManager = UserManager.get(mContext);
}
public String getTileSpec() {
@@ -290,11 +285,12 @@
}
protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) {
- UserHandle user = UserHandle.of(ActivityManager.getCurrentUser());
- if (mUserManager.hasUserRestriction(userRestriction, user)
- && !mUserManager.hasBaseUserRestriction(userRestriction, user)) {
+ EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext,
+ userRestriction, ActivityManager.getCurrentUser());
+ if (admin != null && !RestrictedLockUtils.hasBaseUserRestriction(mContext,
+ userRestriction, ActivityManager.getCurrentUser())) {
state.disabledByPolicy = true;
- state.enforcedAdmin = EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
+ state.enforcedAdmin = admin;
} else {
state.disabledByPolicy = false;
state.enforcedAdmin = null;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index d7777d5..fff7d78 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -428,8 +428,8 @@
}
@Override
- public void onMultiWindowChanged(boolean inMultiWindow) {
- super.onMultiWindowChanged(inMultiWindow);
+ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
+ super.onMultiWindowModeChanged(isInMultiWindowMode);
EventBus.getDefault().send(new ConfigurationChangedEvent(true /* fromMultiWindow */,
false /* fromOrientationChange */));
@@ -449,7 +449,7 @@
mRecentsView.updateStack(loadPlan.getTaskStack());
}
- EventBus.getDefault().send(new MultiWindowStateChangedEvent(inMultiWindow));
+ EventBus.getDefault().send(new MultiWindowStateChangedEvent(isInMultiWindowMode));
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
index 95f26d4c..6a2ecf4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowView.java
@@ -654,7 +654,7 @@
}
@Override
- public void onMultiWindowChanged() {
+ public void onMultiWindowModeChanged() {
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
index 84e785d..ab44b6a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
@@ -654,11 +654,12 @@
}
private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
- UserHandle user = UserHandle.of(ActivityManager.getCurrentUser());
- if (mUserManager.hasUserRestriction(UserManager.DISALLOW_ADD_USER, user)
- && !mUserManager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, user)) {
+ EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext,
+ UserManager.DISALLOW_ADD_USER, ActivityManager.getCurrentUser());
+ if (admin != null && !RestrictedLockUtils.hasBaseUserRestriction(mContext,
+ UserManager.DISALLOW_ADD_USER, ActivityManager.getCurrentUser())) {
record.isDisabledByAdmin = true;
- record.enforcedAdmin = EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
+ record.enforcedAdmin = admin;
} else {
record.isDisabledByAdmin = false;
record.enforcedAdmin = null;
diff --git a/proto/src/metrics_constants.proto b/proto/src/metrics_constants.proto
index 2580e2c..8e4bf24 100644
--- a/proto/src/metrics_constants.proto
+++ b/proto/src/metrics_constants.proto
@@ -2047,6 +2047,39 @@
// Long-press on a QS tile. Tile spec in package field.
ACTION_QS_LONG_PRESS = 366;
+ // OPEN: SUW Welcome Screen -> Vision Settings
+ // CATEGORY: SETTINGS
+ // OS: N
+ SUW_ACCESSIBILITY = 367;
+
+ // OPEN: SUW Welcome Screen -> Vision Settings -> Magnification gesture
+ // ACTION: New magnification gesture configuration is chosen
+ // SUBTYPE: 0 is off, 1 is on
+ // CATEGORY: SETTINGS
+ // OS: N
+ SUW_ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION = 368;
+
+ // OPEN: SUW Welcome Screen -> Vision Settings -> Font size
+ // ACTION: New font size is chosen
+ // SUBTYPE: 0 is small, 1 is default, 2 is large, 3 is largest
+ // CATEGORY: SETTINGS
+ // OS: N
+ SUW_ACCESSIBILITY_FONT_SIZE = 369;
+
+ // OPEN: SUW Welcome Screen -> Vision Settings -> Display size
+ // ACTION: New display size is chosen
+ // SUBTYPE: 0 is small, 1 is default, 2 is large, 3 is larger, 4 is largest
+ // CATEGORY: SETTINGS
+ // OS: N
+ SUW_ACCESSIBILITY_DISPLAY_SIZE = 370;
+
+ // OPEN: SUW Welcome Screen -> Vision Settings -> TalkBack
+ // ACTION: New screen reader configuration is chosen
+ // SUBTYPE: 0 is off, 1 is on
+ // CATEGORY: SETTINGS
+ // OS: N
+ SUW_ACCESSIBILITY_TOGGLE_SCREEN_READER = 371;
+
// Add new aosp constants above this line.
// END OF AOSP CONSTANTS
}
diff --git a/services/core/java/com/android/server/DeviceIdleController.java b/services/core/java/com/android/server/DeviceIdleController.java
index 6023d7f..423f945 100644
--- a/services/core/java/com/android/server/DeviceIdleController.java
+++ b/services/core/java/com/android/server/DeviceIdleController.java
@@ -113,6 +113,7 @@
private AlarmManager mAlarmManager;
private IBatteryStats mBatteryStats;
private PowerManagerInternal mLocalPowerManager;
+ private PowerManager mPowerManager;
private AlarmManagerService.LocalService mLocalAlarmManager;
private INetworkPolicyManager mNetworkPolicyManager;
private DisplayManager mDisplayManager;
@@ -168,16 +169,19 @@
private static final int LIGHT_STATE_ACTIVE = 0;
/** Device is inactive (screen off) and we are waiting to for the first light idle. */
private static final int LIGHT_STATE_INACTIVE = 1;
+ /** Device is about to go idle for the first time, wait for current work to complete. */
+ private static final int LIGHT_STATE_PRE_IDLE = 3;
/** Device is in the light idle state, trying to stay asleep as much as possible. */
- private static final int LIGHT_STATE_IDLE = 2;
+ private static final int LIGHT_STATE_IDLE = 4;
/** Device is in the light idle state, but temporarily out of idle to do regular maintenance. */
- private static final int LIGHT_STATE_IDLE_MAINTENANCE = 3;
+ private static final int LIGHT_STATE_IDLE_MAINTENANCE = 5;
/** Device light idle state is overriden, now applying deep doze state. */
- private static final int LIGHT_STATE_OVERRIDE = 4;
+ private static final int LIGHT_STATE_OVERRIDE = 6;
private static String lightStateToString(int state) {
switch (state) {
case LIGHT_STATE_ACTIVE: return "ACTIVE";
case LIGHT_STATE_INACTIVE: return "INACTIVE";
+ case LIGHT_STATE_PRE_IDLE: return "PRE_IDLE";
case LIGHT_STATE_IDLE: return "IDLE";
case LIGHT_STATE_IDLE_MAINTENANCE: return "IDLE_MAINTENANCE";
case LIGHT_STATE_OVERRIDE: return "OVERRIDE";
@@ -198,6 +202,7 @@
private long mMaintenanceStartTime;
private int mActiveIdleOpCount;
+ private PowerManager.WakeLock mActiveIdleWakeLock;
private IBinder mDownloadServiceActive;
private boolean mJobsActive;
private boolean mAlarmsActive;
@@ -344,21 +349,18 @@
}
};
- private final AlarmManager.OnAlarmListener mMaintenanceMinCheckListener
- = new AlarmManager.OnAlarmListener() {
- @Override
- public void onAlarm() {
- synchronized (DeviceIdleController.this) {
- exitMaintenanceEarlyIfNeededLocked();
- }
- }
- };
-
- private boolean mMaintenanceMinCheckScheduled;
-
private final BroadcastReceiver mIdleStartedDoneReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
- decActiveIdleOps();
+ // When coming out of a deep idle, we will add in some delay before we allow
+ // the system to settle down and finish the maintenance window. This is
+ // to give a chance for any pending work to be scheduled.
+ if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction())) {
+ mHandler.sendEmptyMessageDelayed(MSG_FINISH_IDLE_OP,
+ mConstants.MIN_DEEP_MAINTENANCE_TIME);
+ } else {
+ mHandler.sendEmptyMessageDelayed(MSG_FINISH_IDLE_OP,
+ mConstants.MIN_LIGHT_MAINTENANCE_TIME);
+ }
}
};
@@ -482,6 +484,7 @@
// Key names stored in the settings value.
private static final String KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT
= "light_after_inactive_to";
+ private static final String KEY_LIGHT_PRE_IDLE_TIMEOUT = "light_pre_idle_to";
private static final String KEY_LIGHT_IDLE_TIMEOUT = "light_idle_to";
private static final String KEY_LIGHT_IDLE_FACTOR = "light_idle_factor";
private static final String KEY_LIGHT_MAX_IDLE_TIMEOUT = "light_max_idle_to";
@@ -520,6 +523,15 @@
public long LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT;
/**
+ * This is amount of time we will wait from the point where we decide we would
+ * like to go idle until we actually do, while waiting for jobs and other current
+ * activity to finish.
+ * @see Settings.Global#DEVICE_IDLE_CONSTANTS
+ * @see #KEY_LIGHT_PRE_IDLE_TIMEOUT
+ */
+ public long LIGHT_PRE_IDLE_TIMEOUT;
+
+ /**
* This is the initial time that we will run in idle maintenance mode.
* @see Settings.Global#DEVICE_IDLE_CONSTANTS
* @see #KEY_LIGHT_IDLE_TIMEOUT
@@ -747,6 +759,8 @@
LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getLong(
KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT,
!COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L);
+ LIGHT_PRE_IDLE_TIMEOUT = mParser.getLong(KEY_LIGHT_PRE_IDLE_TIMEOUT,
+ !COMPRESS_TIME ? 10 * 60 * 1000L : 30 * 1000L);
LIGHT_IDLE_TIMEOUT = mParser.getLong(KEY_LIGHT_IDLE_TIMEOUT,
!COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L);
LIGHT_IDLE_FACTOR = mParser.getFloat(KEY_LIGHT_IDLE_FACTOR,
@@ -809,6 +823,10 @@
TimeUtils.formatDuration(LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT, pw);
pw.println();
+ pw.print(" "); pw.print(KEY_LIGHT_PRE_IDLE_TIMEOUT); pw.print("=");
+ TimeUtils.formatDuration(LIGHT_PRE_IDLE_TIMEOUT, pw);
+ pw.println();
+
pw.print(" "); pw.print(KEY_LIGHT_IDLE_TIMEOUT); pw.print("=");
TimeUtils.formatDuration(LIGHT_IDLE_TIMEOUT, pw);
pw.println();
@@ -939,6 +957,7 @@
static final int MSG_REPORT_ACTIVE = 5;
static final int MSG_TEMP_APP_WHITELIST_TIMEOUT = 6;
static final int MSG_REPORT_MAINTENANCE_ACTIVITY = 7;
+ static final int MSG_FINISH_IDLE_OP = 8;
final class MyHandler extends Handler {
MyHandler(Looper looper) {
@@ -1043,6 +1062,9 @@
mMaintenanceActivityListeners.finishBroadcast();
}
} break;
+ case MSG_FINISH_IDLE_OP: {
+ decActiveIdleOps();
+ } break;
}
}
}
@@ -1299,6 +1321,10 @@
mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
mBatteryStats = BatteryStatsService.getService();
mLocalPowerManager = getLocalService(PowerManagerInternal.class);
+ mPowerManager = getContext().getSystemService(PowerManager.class);
+ mActiveIdleWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "deviceidle_maint");
+ mActiveIdleWakeLock.setReferenceCounted(false);
mLocalAlarmManager = getLocalService(AlarmManagerService.LocalService.class);
mNetworkPolicyManager = INetworkPolicyManager.Stub.asInterface(
ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
@@ -1687,10 +1713,6 @@
mInactiveTimeout = mConstants.INACTIVE_TIMEOUT;
mCurIdleBudget = 0;
mMaintenanceStartTime = 0;
- if (mMaintenanceMinCheckScheduled) {
- mAlarmManager.cancel(mMaintenanceMinCheckListener);
- mMaintenanceMinCheckScheduled = false;
- }
resetIdleManagementLocked();
resetLightIdleManagementLocked();
addEvent(EVENT_NORMAL);
@@ -1758,6 +1780,16 @@
// Reset the upcoming idle delays.
mNextLightIdleDelay = mConstants.LIGHT_IDLE_TIMEOUT;
mMaintenanceStartTime = 0;
+ if (!isOpsInactiveLocked()) {
+ // We have some active ops going on... give them a chance to finish
+ // before going in to our first idle.
+ mLightState = LIGHT_STATE_PRE_IDLE;
+ EventLogTags.writeDeviceIdleLight(mLightState, reason);
+ scheduleLightAlarmLocked(mConstants.LIGHT_PRE_IDLE_TIMEOUT);
+ break;
+ }
+ // Nothing active, fall through to immediately idle.
+ case LIGHT_STATE_PRE_IDLE:
case LIGHT_STATE_IDLE_MAINTENANCE:
if (mMaintenanceStartTime != 0) {
long duration = SystemClock.elapsedRealtime() - mMaintenanceStartTime;
@@ -1781,14 +1813,11 @@
EventLogTags.writeDeviceIdleLight(mLightState, reason);
addEvent(EVENT_LIGHT_IDLE);
mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON_LIGHT);
- if (mMaintenanceMinCheckScheduled) {
- mAlarmManager.cancel(mMaintenanceMinCheckListener);
- mMaintenanceMinCheckScheduled = false;
- }
break;
case LIGHT_STATE_IDLE:
// We have been idling long enough, now it is time to do some work.
mActiveIdleOpCount = 1;
+ mActiveIdleWakeLock.acquire();
mMaintenanceStartTime = SystemClock.elapsedRealtime();
if (mCurIdleBudget < mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET) {
mCurIdleBudget = mConstants.LIGHT_IDLE_MAINTENANCE_MIN_BUDGET;
@@ -1802,10 +1831,6 @@
EventLogTags.writeDeviceIdleLight(mLightState, reason);
addEvent(EVENT_LIGHT_MAINTENANCE);
mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
- mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME,
- mMaintenanceStartTime + mConstants.MIN_LIGHT_MAINTENANCE_TIME,
- "DeviceIdleController.maint-check", mMaintenanceMinCheckListener, mHandler);
- mMaintenanceMinCheckScheduled = true;
break;
}
}
@@ -1901,14 +1926,11 @@
EventLogTags.writeDeviceIdle(mState, reason);
addEvent(EVENT_DEEP_IDLE);
mHandler.sendEmptyMessage(MSG_REPORT_IDLE_ON);
- if (mMaintenanceMinCheckScheduled) {
- mAlarmManager.cancel(mMaintenanceMinCheckListener);
- mMaintenanceMinCheckScheduled = false;
- }
break;
case STATE_IDLE:
// We have been idling long enough, now it is time to do some work.
mActiveIdleOpCount = 1;
+ mActiveIdleWakeLock.acquire();
scheduleAlarmLocked(mNextIdlePendingDelay, false);
if (DEBUG) Slog.d(TAG, "Moved from STATE_IDLE to STATE_IDLE_MAINTENANCE. " +
"Next alarm in " + mNextIdlePendingDelay + " ms.");
@@ -1922,10 +1944,6 @@
EventLogTags.writeDeviceIdle(mState, reason);
addEvent(EVENT_DEEP_MAINTENANCE);
mHandler.sendEmptyMessage(MSG_REPORT_IDLE_OFF);
- mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME,
- mMaintenanceStartTime + mConstants.MIN_DEEP_MAINTENANCE_TIME,
- "DeviceIdleController.maint-check", mMaintenanceMinCheckListener, mHandler);
- mMaintenanceMinCheckScheduled = true;
break;
}
}
@@ -1941,6 +1959,7 @@
mActiveIdleOpCount--;
if (mActiveIdleOpCount <= 0) {
exitMaintenanceEarlyIfNeededLocked();
+ mActiveIdleWakeLock.release();
}
}
}
@@ -2012,10 +2031,15 @@
mHandler.sendMessage(msg);
}
+ boolean isOpsInactiveLocked() {
+ return mActiveIdleOpCount <= 0 && mDownloadServiceActive == null
+ && !mJobsActive && !mAlarmsActive;
+ }
+
void exitMaintenanceEarlyIfNeededLocked() {
- if (mState == STATE_IDLE_MAINTENANCE || mLightState == LIGHT_STATE_IDLE_MAINTENANCE) {
- if (mActiveIdleOpCount <= 0 && mDownloadServiceActive == null
- && !mJobsActive && !mAlarmsActive) {
+ if (mState == STATE_IDLE_MAINTENANCE || mLightState == LIGHT_STATE_IDLE_MAINTENANCE
+ || mLightState == LIGHT_STATE_PRE_IDLE) {
+ if (isOpsInactiveLocked()) {
final long now = SystemClock.elapsedRealtime();
if (DEBUG) {
StringBuilder sb = new StringBuilder();
@@ -2026,13 +2050,11 @@
Slog.d(TAG, sb.toString());
}
if (mState == STATE_IDLE_MAINTENANCE) {
- if (now >= (mMaintenanceStartTime + mConstants.MIN_DEEP_MAINTENANCE_TIME)) {
- stepIdleStateLocked("s:early");
- }
+ stepIdleStateLocked("s:early");
+ } else if (mLightState == LIGHT_STATE_PRE_IDLE) {
+ stepLightIdleStateLocked("s:predone");
} else {
- if (now >= (mMaintenanceStartTime + mConstants.MIN_LIGHT_MAINTENANCE_TIME)) {
- stepLightIdleStateLocked("s:early");
- }
+ stepLightIdleStateLocked("s:early");
}
}
}
@@ -2828,10 +2850,6 @@
TimeUtils.formatDuration(mMaintenanceStartTime, SystemClock.elapsedRealtime(), pw);
pw.println();
}
- if (mMaintenanceMinCheckScheduled) {
- pw.print(" mMaintenanceMinCheckScheduled=");
- pw.println(mMaintenanceMinCheckScheduled);
- }
if (mJobsActive) {
pw.print(" mJobsActive="); pw.println(mJobsActive);
}
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index 9e2f1167..994e1d0 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -1231,7 +1231,8 @@
}
final Intent intent = new Intent(DiskInfo.ACTION_DISK_SCANNED);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
intent.putExtra(DiskInfo.EXTRA_DISK_ID, disk.id);
intent.putExtra(DiskInfo.EXTRA_VOLUME_COUNT, volumeCount);
mHandler.obtainMessage(H_INTERNAL_BROADCAST, intent).sendToTarget();
@@ -1347,7 +1348,8 @@
intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id);
intent.putExtra(VolumeInfo.EXTRA_VOLUME_STATE, newState);
intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.fsUuid);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
mHandler.obtainMessage(H_INTERNAL_BROADCAST, intent).sendToTarget();
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 4bb3a54..42cf42f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -351,7 +351,6 @@
import static com.android.server.am.TaskRecord.LOCK_TASK_AUTH_PINNABLE;
import static com.android.server.wm.AppTransition.TRANSIT_ACTIVITY_OPEN;
import static com.android.server.wm.AppTransition.TRANSIT_ACTIVITY_RELAUNCH;
-import static com.android.server.wm.AppTransition.TRANSIT_DOCK_TASK_FROM_RECENTS;
import static com.android.server.wm.AppTransition.TRANSIT_TASK_IN_PLACE;
import static com.android.server.wm.AppTransition.TRANSIT_TASK_OPEN;
import static com.android.server.wm.AppTransition.TRANSIT_TASK_TO_FRONT;
@@ -7231,7 +7230,7 @@
}
@Override
- public boolean inMultiWindow(IBinder token) {
+ public boolean isInMultiWindowMode(IBinder token) {
final long origId = Binder.clearCallingIdentity();
try {
synchronized(this) {
@@ -7248,7 +7247,7 @@
}
@Override
- public boolean inPictureInPicture(IBinder token) {
+ public boolean isInPictureInPictureMode(IBinder token) {
final long origId = Binder.clearCallingIdentity();
try {
synchronized(this) {
@@ -7264,24 +7263,24 @@
}
@Override
- public void enterPictureInPicture(IBinder token) {
+ public void enterPictureInPictureMode(IBinder token) {
final long origId = Binder.clearCallingIdentity();
try {
synchronized(this) {
if (!mSupportsPictureInPicture) {
- throw new IllegalStateException("enterPictureInPicture: "
+ throw new IllegalStateException("enterPictureInPictureMode: "
+ "Device doesn't support picture-in-picture mode.");
}
final ActivityRecord r = ActivityRecord.forTokenLocked(token);
if (r == null) {
- throw new IllegalStateException("enterPictureInPicture: "
+ throw new IllegalStateException("enterPictureInPictureMode: "
+ "Can't find activity for token=" + token);
}
if (!r.supportsPictureInPicture()) {
- throw new IllegalArgumentException("enterPictureInPicture: "
+ throw new IllegalArgumentException("enterPictureInPictureMode: "
+ "Picture-In-Picture not supported for r=" + r);
}
@@ -7290,7 +7289,7 @@
? mDefaultPinnedStackBounds : null;
mStackSupervisor.moveActivityToPinnedStackLocked(
- r, "enterPictureInPicture", bounds);
+ r, "enterPictureInPictureMode", bounds);
}
} finally {
Binder.restoreCallingIdentity(origId);
diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java
index 5219827..48f87b6 100755
--- a/services/core/java/com/android/server/am/ActivityRecord.java
+++ b/services/core/java/com/android/server/am/ActivityRecord.java
@@ -23,7 +23,6 @@
import static android.content.pm.ActivityInfo.FLAG_ALWAYS_FOCUSABLE;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE_AND_PIPABLE;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_CONFIGURATION;
-import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_SAVED_STATE;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_SWITCH;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_THUMBNAILS;
import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_STATES;
@@ -452,24 +451,24 @@
}
}
- void scheduleMultiWindowChanged() {
+ void scheduleMultiWindowModeChanged() {
if (task == null || task.stack == null || app == null || app.thread == null) {
return;
}
try {
// An activity is considered to be in multi-window mode if its task isn't fullscreen.
- app.thread.scheduleMultiWindowChanged(appToken, !task.mFullscreen);
+ app.thread.scheduleMultiWindowModeChanged(appToken, !task.mFullscreen);
} catch (Exception e) {
// If process died, I don't care.
}
}
- void schedulePictureInPictureChanged() {
+ void schedulePictureInPictureModeChanged() {
if (task == null || task.stack == null || app == null || app.thread == null) {
return;
}
try {
- app.thread.schedulePictureInPictureChanged(
+ app.thread.schedulePictureInPictureModeChanged(
appToken, task.stack.mStackId == PINNED_STACK_ID);
} catch (Exception e) {
// If process died, no one cares.
diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java
index a9ef1d6..b297938 100644
--- a/services/core/java/com/android/server/am/ActivityStack.java
+++ b/services/core/java/com/android/server/am/ActivityStack.java
@@ -4986,7 +4986,7 @@
private void postAddTask(TaskRecord task, ActivityStack prevStack) {
if (prevStack != null) {
- mStackSupervisor.scheduleReportPictureInPictureChangedIfNeeded(task, prevStack);
+ mStackSupervisor.scheduleReportPictureInPictureModeChangedIfNeeded(task, prevStack);
} else if (task.voiceSession != null) {
try {
task.voiceSession.taskStarted(task.intent, task.taskId);
@@ -5045,7 +5045,7 @@
r.setTask(task, null);
task.addActivityToTop(r);
setAppTask(r, task);
- mStackSupervisor.scheduleReportPictureInPictureChangedIfNeeded(task, prevStack);
+ mStackSupervisor.scheduleReportPictureInPictureModeChangedIfNeeded(task, prevStack);
moveToFrontAndResumeStateIfNeeded(r, wasFocused, wasResumed, "moveActivityToStack");
if (wasResumed) {
prevStack.mResumedActivity = null;
diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
index 950320e..0d70e99 100644
--- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java
@@ -102,7 +102,6 @@
import java.util.Objects;
import java.util.Set;
-import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS;
import static android.Manifest.permission.START_ANY_ACTIVITY;
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
@@ -3524,7 +3523,7 @@
mActivityMetricsLogger.logWindowState();
}
- void scheduleReportMultiWindowChanged(TaskRecord task) {
+ void scheduleReportMultiWindowModeChanged(TaskRecord task) {
for (int i = task.mActivities.size() - 1; i >= 0; i--) {
final ActivityRecord r = task.mActivities.get(i);
if (r.app != null && r.app.thread != null) {
@@ -3537,7 +3536,7 @@
}
}
- void scheduleReportPictureInPictureChangedIfNeeded(TaskRecord task, ActivityStack prevStack) {
+ void scheduleReportPictureInPictureModeChangedIfNeeded(TaskRecord task, ActivityStack prevStack) {
final ActivityStack stack = task.stack;
if (prevStack == null || prevStack == stack
|| (prevStack.mStackId != PINNED_STACK_ID && stack.mStackId != PINNED_STACK_ID)) {
@@ -3575,7 +3574,7 @@
synchronized (mService) {
for (int i = mMultiWindowModeChangedActivities.size() - 1; i >= 0; i--) {
final ActivityRecord r = mMultiWindowModeChangedActivities.remove(i);
- r.scheduleMultiWindowChanged();
+ r.scheduleMultiWindowModeChanged();
}
}
} break;
@@ -3583,7 +3582,7 @@
synchronized (mService) {
for (int i = mPipModeChangedActivities.size() - 1; i >= 0; i--) {
final ActivityRecord r = mPipModeChangedActivities.remove(i);
- r.schedulePictureInPictureChanged();
+ r.schedulePictureInPictureModeChanged();
}
}
} break;
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index 0f1ebeb..b157070 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -1449,7 +1449,7 @@
}
if (mFullscreen != oldFullscreen) {
- mService.mStackSupervisor.scheduleReportMultiWindowChanged(this);
+ mService.mStackSupervisor.scheduleReportMultiWindowModeChanged(this);
}
return !mOverrideConfig.equals(oldConfig) ? mOverrideConfig : null;
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index c59591e9..aef454e 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -434,35 +434,17 @@
stoppingIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
stoppingIntent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
stoppingIntent.putExtra(Intent.EXTRA_SHUTDOWN_USERSPACE_ONLY, true);
- final Intent shutdownIntent = new Intent(Intent.ACTION_SHUTDOWN);
- // This is the result receiver for the final shutdown broadcast.
- final IIntentReceiver shutdownReceiver = new IIntentReceiver.Stub() {
- @Override
- public void performReceive(Intent intent, int resultCode, String data,
- Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
- finishUserStop(uss);
- }
- };
// This is the result receiver for the initial stopping broadcast.
final IIntentReceiver stoppingReceiver = new IIntentReceiver.Stub() {
@Override
public void performReceive(Intent intent, int resultCode, String data,
Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
- // On to the next.
- synchronized (mService) {
- if (uss.state != UserState.STATE_STOPPING) {
- // Whoops, we are being started back up. Abort, abort!
- return;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ finishUserStopping(userId, uss);
}
- uss.setState(UserState.STATE_SHUTDOWN);
- }
- mService.mBatteryStatsService.noteEvent(
- BatteryStats.HistoryItem.EVENT_USER_RUNNING_FINISH,
- Integer.toString(userId), userId);
- mService.mSystemServiceManager.stopUser(userId);
- mService.broadcastIntentLocked(null, null, shutdownIntent,
- null, shutdownReceiver, 0, null, null, null, AppOpsManager.OP_NONE,
- null, true, false, MY_PID, SYSTEM_UID, userId);
+ });
}
};
// Kick things off.
@@ -476,7 +458,45 @@
}
}
- void finishUserStop(UserState uss) {
+ void finishUserStopping(final int userId, final UserState uss) {
+ // On to the next.
+ final Intent shutdownIntent = new Intent(Intent.ACTION_SHUTDOWN);
+ // This is the result receiver for the final shutdown broadcast.
+ final IIntentReceiver shutdownReceiver = new IIntentReceiver.Stub() {
+ @Override
+ public void performReceive(Intent intent, int resultCode, String data,
+ Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ finishUserStopped(uss);
+ }
+ });
+ }
+ };
+
+ synchronized (mService) {
+ if (uss.state != UserState.STATE_STOPPING) {
+ // Whoops, we are being started back up. Abort, abort!
+ return;
+ }
+ uss.setState(UserState.STATE_SHUTDOWN);
+ }
+
+ mService.mBatteryStatsService.noteEvent(
+ BatteryStats.HistoryItem.EVENT_USER_RUNNING_FINISH,
+ Integer.toString(userId), userId);
+ mService.mSystemServiceManager.stopUser(userId);
+
+ synchronized (mService) {
+ mService.broadcastIntentLocked(null, null, shutdownIntent,
+ null, shutdownReceiver, 0, null, null, null,
+ AppOpsManager.OP_NONE,
+ null, true, false, MY_PID, SYSTEM_UID, userId);
+ }
+ }
+
+ void finishUserStopped(UserState uss) {
final int userId = uss.mHandle.getIdentifier();
boolean stopped;
ArrayList<IStopUserCallback> callbacks;
@@ -765,10 +785,17 @@
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
mService.broadcastIntentLocked(null, null, intent, null,
new IIntentReceiver.Stub() {
+ @Override
public void performReceive(Intent intent, int resultCode,
String data, Bundle extras, boolean ordered,
boolean sticky, int sendingUser) {
- onUserInitialized(uss, foreground, oldUserId, userId);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onUserInitialized(uss, foreground,
+ oldUserId, userId);
+ }
+ });
}
}, 0, null, null, null, AppOpsManager.OP_NONE,
null, true, false, MY_PID, SYSTEM_UID, userId);
diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java
index 28170f2..7f7ea9d 100644
--- a/services/core/java/com/android/server/content/ContentService.java
+++ b/services/core/java/com/android/server/content/ContentService.java
@@ -26,9 +26,9 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentService;
+import android.content.ISyncStatusObserver;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.ISyncStatusObserver;
import android.content.PeriodicSync;
import android.content.SyncAdapterType;
import android.content.SyncInfo;
@@ -58,18 +58,15 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.IndentingPrintWriter;
-import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.security.InvalidParameterException;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
-import java.util.Objects;
/**
* {@hide}
@@ -1030,14 +1027,16 @@
if (uri != null) {
for (int i = 0; i < packageCache.size();) {
- final Uri key = packageCache.keyAt(i).second;
- if (Objects.equals(key, uri)) {
+ final Pair<String, Uri> key = packageCache.keyAt(i);
+ if (key.second != null && key.second.toString().startsWith(uri.toString())) {
+ Slog.d(TAG, "Invalidating cache for key " + key);
packageCache.removeAt(i);
} else {
i++;
}
}
} else {
+ Slog.d(TAG, "Invalidating cache for package " + providerPackageName);
packageCache.clear();
}
}
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index c75e287..2595864 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -49,7 +49,9 @@
import static android.net.NetworkPolicyManager.POLICY_NONE;
import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
import static android.net.NetworkPolicyManager.RULE_ALLOW_ALL;
+import static android.net.NetworkPolicyManager.RULE_ALLOW_METERED;
import static android.net.NetworkPolicyManager.RULE_REJECT_METERED;
+import static android.net.NetworkPolicyManager.RULE_TEMPORARY_ALLOW_METERED;
import static android.net.NetworkPolicyManager.RULE_UNKNOWN;
import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
import static android.net.NetworkTemplate.MATCH_MOBILE_3G_LOWER;
@@ -1729,7 +1731,7 @@
if (wlUids.length > 0) {
for (int uid : wlUids) {
- removeRestrictBackgroundWhitelistedUidLocked(uid, false);
+ removeRestrictBackgroundWhitelistedUidLocked(uid, false, false);
}
writePolicy = true;
}
@@ -1896,10 +1898,12 @@
try {
maybeRefreshTrustedTime();
synchronized (mRulesLock) {
- mRestrictBackground = restrictBackground;
- updateRulesForRestrictBackgroundLocked();
- updateNotificationsLocked();
- writePolicyLocked();
+ if (restrictBackground == mRestrictBackground) {
+ // Ideally, UI should never allow this scenario...
+ Slog.w(TAG, "setRestrictBackground: already " + restrictBackground);
+ return;
+ }
+ setRestrictBackgroundLocked(restrictBackground);
}
} finally {
@@ -1910,17 +1914,42 @@
.sendToTarget();
}
+ private void setRestrictBackgroundLocked(boolean restrictBackground) {
+ final boolean oldRestrictBackground = mRestrictBackground;
+ mRestrictBackground = restrictBackground;
+ // Must whitelist foreground apps before turning data saver mode on.
+ // TODO: there is no need to iterate through all apps here, just those in the foreground,
+ // so it could call AM to get the UIDs of such apps, and iterate through them instead.
+ updateRulesForRestrictBackgroundLocked();
+ try {
+ if (!mNetworkManager.setDataSaverModeEnabled(mRestrictBackground)) {
+ Slog.e(TAG, "Could not change Data Saver Mode on NMS to " + mRestrictBackground);
+ mRestrictBackground = oldRestrictBackground;
+ // TODO: if it knew the foreground apps (see TODO above), it could call
+ // updateRulesForRestrictBackgroundLocked() again to restore state.
+ return;
+ }
+ } catch (RemoteException e) {
+ // ignored; service lives in system_server
+ }
+ updateNotificationsLocked();
+ writePolicyLocked();
+ }
+
@Override
public void addRestrictBackgroundWhitelistedUid(int uid) {
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
- if (!isUidValidForRules(uid)) return;
- final boolean changed;
+ final boolean oldStatus;
synchronized (mRulesLock) {
- final boolean oldStatus = mRestrictBackgroundWhitelistUids.get(uid);
+ oldStatus = mRestrictBackgroundWhitelistUids.get(uid);
if (oldStatus) {
if (LOGD) Slog.d(TAG, "uid " + uid + " is already whitelisted");
return;
}
+ if (!isUidValidForWhitelistRules(uid)) {
+ if (LOGD) Slog.d(TAG, "no need to whitelist uid " + uid);
+ return;
+ }
Slog.i(TAG, "adding uid " + uid + " to restrict background whitelist");
mRestrictBackgroundWhitelistUids.append(uid, true);
if (mDefaultRestrictBackgroundWhitelistUids.get(uid)
@@ -1929,13 +1958,10 @@
+ " from revoked restrict background whitelist");
mRestrictBackgroundWhitelistRevokedUids.delete(uid);
}
- changed = mRestrictBackground && !oldStatus;
- if (changed && hasInternetPermissions(uid)) {
- setUidNetworkRules(uid, false);
- }
+ updateRuleForRestrictBackgroundLocked(uid);
writePolicyLocked();
}
- if (changed) {
+ if (mRestrictBackground && !oldStatus) {
mHandler.obtainMessage(MSG_RESTRICT_BACKGROUND_WHITELIST_CHANGED, uid, 0)
.sendToTarget();
}
@@ -1944,10 +1970,9 @@
@Override
public void removeRestrictBackgroundWhitelistedUid(int uid) {
mContext.enforceCallingOrSelfPermission(MANAGE_NETWORK_POLICY, TAG);
- if (!isUidValidForRules(uid)) return;
final boolean changed;
synchronized (mRulesLock) {
- changed = removeRestrictBackgroundWhitelistedUidLocked(uid, true);
+ changed = removeRestrictBackgroundWhitelistedUidLocked(uid, false, true);
}
if (changed) {
mHandler.obtainMessage(MSG_RESTRICT_BACKGROUND_WHITELIST_CHANGED, uid, 0)
@@ -1955,14 +1980,22 @@
}
}
- private boolean removeRestrictBackgroundWhitelistedUidLocked(int uid, boolean updateNow) {
+ /**
+ * Removes a uid from the restricted background whitelist, returning whether its current
+ * {@link ConnectivityManager.RestrictBackgroundStatus} changed.
+ */
+ private boolean removeRestrictBackgroundWhitelistedUidLocked(int uid, boolean uidDeleted,
+ boolean updateNow) {
final boolean oldStatus = mRestrictBackgroundWhitelistUids.get(uid);
if (!oldStatus) {
if (LOGD) Slog.d(TAG, "uid " + uid + " was not whitelisted before");
return false;
}
+ if (!uidDeleted && !isUidValidForWhitelistRules(uid)) {
+ if (LOGD) Slog.d(TAG, "no need to remove whitelist for uid " + uid);
+ return false;
+ }
Slog.i(TAG, "removing uid " + uid + " from restrict background whitelist");
- final boolean changed = mRestrictBackground && oldStatus;
mRestrictBackgroundWhitelistUids.delete(uid);
if (mDefaultRestrictBackgroundWhitelistUids.get(uid)
&& !mRestrictBackgroundWhitelistRevokedUids.get(uid)) {
@@ -1970,13 +2003,13 @@
+ " to revoked restrict background whitelist");
mRestrictBackgroundWhitelistRevokedUids.append(uid, true);
}
+ updateRuleForRestrictBackgroundLocked(uid, uidDeleted);
if (updateNow) {
- if (changed && hasInternetPermissions(uid)) {
- setUidNetworkRules(uid, true);
- }
writePolicyLocked();
}
- return changed;
+ // Status only changes if Data Saver is turned on (otherwise it is DISABLED, even if the
+ // app was whitelisted before).
+ return mRestrictBackground;
}
@Override
@@ -2268,7 +2301,12 @@
final int state = mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
fout.print(" state=");
fout.print(state);
- fout.print(state <= ActivityManager.PROCESS_STATE_TOP ? " (fg)" : " (bg)");
+ if (state <= ActivityManager.PROCESS_STATE_TOP) {
+ fout.print(" (fg)");
+ } else {
+ fout.print(state <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+ ? " (fg svc)" : " (bg)");
+ }
final int rule = mUidRules.get(uid, RULE_UNKNOWN);
fout.print(" rule=");
@@ -2305,6 +2343,11 @@
mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY));
}
+ private boolean isUidForegroundOnRestrictBackgroundLocked(int uid) {
+ final int procState = mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
+ return isProcStateAllowedWhileOnRestrictBackgroundLocked(procState);
+ }
+
private boolean isUidStateForegroundLocked(int state) {
// only really in foreground when screen is also on
return mScreenOn && state <= ActivityManager.PROCESS_STATE_TOP;
@@ -2312,7 +2355,7 @@
/**
* Process state of UID changed; if needed, will trigger
- * {@link #updateRestrictDataRulesForUidLocked(int)}.
+ * {@link #updateRuleForRestrictBackgroundLocked(int)}.
*/
private void updateUidStateLocked(int uid, int uidState) {
final int oldUidState = mUidState.get(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
@@ -2363,8 +2406,10 @@
private void updateRestrictBackgroundRulesOnUidStatusChangedLocked(int uid, int oldUidState,
int newUidState) {
- final boolean oldForeground = oldUidState <= ActivityManager.PROCESS_STATE_TOP;
- final boolean newForeground = newUidState <= ActivityManager.PROCESS_STATE_TOP;
+ final boolean oldForeground =
+ isProcStateAllowedWhileOnRestrictBackgroundLocked(oldUidState);
+ final boolean newForeground =
+ isProcStateAllowedWhileOnRestrictBackgroundLocked(newUidState);
if (oldForeground != newForeground) {
updateRuleForRestrictBackgroundLocked(uid);
}
@@ -2388,7 +2433,7 @@
// only update rules for anyone with foreground activities
final int size = mUidState.size();
for (int i = 0; i < size; i++) {
- if (mUidState.valueAt(i) <= ActivityManager.PROCESS_STATE_TOP) {
+ if (mUidState.valueAt(i) <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
final int uid = mUidState.keyAt(i);
updateRestrictionRulesForUidLocked(uid);
}
@@ -2399,6 +2444,10 @@
return procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
}
+ static boolean isProcStateAllowedWhileOnRestrictBackgroundLocked(int procState) {
+ return procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ }
+
void updateRulesForRestrictPowerLocked() {
updateRulesForWhitelistedPowerSaveLocked(mRestrictPower, FIREWALL_CHAIN_POWERSAVE,
mUidFirewallPowerSaveRules);
@@ -2492,7 +2541,7 @@
}
void updateRuleForAppIdleLocked(int uid) {
- if (!isUidValidForRules(uid)) return;
+ if (!isUidValidForBlacklistRules(uid)) return;
int appId = UserHandle.getAppId(uid);
if (!mPowerSaveTempWhitelistAppIds.get(appId) && isUidIdle(uid)) {
@@ -2519,6 +2568,7 @@
updateRulesForAppIdleLocked();
updateRulesForRestrictPowerLocked();
updateRulesForRestrictBackgroundLocked();
+ setRestrictBackgroundLocked(mRestrictBackground);
// If the set of restricted networks may have changed, re-evaluate those.
if (restrictedNetworksChanged) {
@@ -2552,10 +2602,6 @@
updateRuleForRestrictBackgroundLocked(uid);
}
}
-
- // limit data usage for some internal system services
- updateRuleForRestrictBackgroundLocked(android.os.Process.MEDIA_UID);
- updateRuleForRestrictBackgroundLocked(android.os.Process.DRM_UID);
}
private void updateRulesForTempWhitelistChangeLocked() {
@@ -2572,16 +2618,22 @@
}
}
- private boolean isUidValidForRules(int uid) {
- // allow rules on specific system services, and any apps (that have network access)
+ // TODO: the MEDIA / DRM restriction might not be needed anymore, in which case both
+ // methods below could be merged into a isUidValidForRules() method.
+ private boolean isUidValidForBlacklistRules(int uid) {
+ // allow rules on specific system services, and any apps
if (uid == android.os.Process.MEDIA_UID || uid == android.os.Process.DRM_UID
- || (UserHandle.isApp(uid) && hasInternetPermissions(uid))) {
+ || (UserHandle.isApp(uid) && hasInternetPermissions(uid))) {
return true;
}
return false;
}
+ private boolean isUidValidForWhitelistRules(int uid) {
+ return UserHandle.isApp(uid) && hasInternetPermissions(uid);
+ }
+
private boolean isUidIdle(int uid) {
final String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
final int userId = UserHandle.getUserId(uid);
@@ -2626,45 +2678,142 @@
updateRuleForRestrictBackgroundLocked(uid);
}
+ /**
+ * Applies network rules to bandwidth controllers based on process state and user-defined
+ * restrictions (blacklist / whitelist).
+ *
+ * <p>
+ * {@code netd} defines 3 firewall chains that govern whether an app has access to metered
+ * networks:
+ * <ul>
+ * <li>@{code bw_penalty_box}: UIDs added to this chain do not have access (blacklist).
+ * <li>@{code bw_happy_box}: UIDs added to this chain have access (whitelist), unless they're
+ * also blacklisted.
+ * <li>@{code bw_data_saver}: when enabled (through {@link #setRestrictBackground(boolean)}),
+ * no UIDs other those whitelisted will have access.
+ * <ul>
+ *
+ * <p>The @{code bw_penalty_box} and @{code bw_happy_box} are primarily managed through the
+ * {@link #setUidPolicy(int, int)} and {@link #addRestrictBackgroundWhitelistedUid(int)} /
+ * {@link #removeRestrictBackgroundWhitelistedUid(int)} methods (for blacklist and whitelist
+ * respectively): these methods set the proper internal state (blacklist / whitelist), then call
+ * this ({@link #updateRuleForRestrictBackgroundLocked(int)}) to propagate the rules to
+ * {@link INetworkManagementService}, but this method should also be called in events (like
+ * Data Saver Mode flips or UID state changes) that might affect the foreground app, since the
+ * following rules should also be applied:
+ *
+ * <ul>
+ * <li>When Data Saver mode is on, the foreground app should be temporarily added to
+ * {@code bw_happy_box} before the @{code bw_data_saver} chain is enabled.
+ * <li>If the foreground app is blacklisted by the user, it should be temporarily removed from
+ * {@code bw_penalty_box}.
+ * <li>When the app leaves foreground state, the temporary changes above should be reverted.
+ * </ul>
+ *
+ * <p>For optimization, the rules are only applied on user apps that have internet access
+ * permission, since there is no need to change the {@code iptables} rule if the app does not
+ * have permission to use the internet.
+ *
+ * <p>The {@link #mUidRules} map is used to define the transtion of states of an UID.
+ */
private void updateRuleForRestrictBackgroundLocked(int uid) {
- if (!isUidValidForRules(uid)) return;
+ updateRuleForRestrictBackgroundLocked(uid, false);
+ }
+
+ /**
+ * Overloaded version of {@link #updateRuleForRestrictBackgroundLocked(int)} called when an
+ * app is removed - it ignores the UID validity check.
+ */
+ private void updateRuleForRestrictBackgroundLocked(int uid, boolean uidDeleted) {
+ if (!uidDeleted && !isUidValidForWhitelistRules(uid)) {
+ if (LOGD) Slog.d(TAG, "no need to update restrict data rules for uid " + uid);
+ return;
+ }
final int uidPolicy = mUidPolicy.get(uid, POLICY_NONE);
- final boolean uidForeground = isUidForegroundLocked(uid);
+ final boolean isForeground = isUidForegroundOnRestrictBackgroundLocked(uid);
+ final boolean isBlacklisted = (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0;
+ final boolean isWhitelisted = mRestrictBackgroundWhitelistUids.get(uid);
- // Derive active rules based on policy and active state
int newRule = RULE_ALLOW_ALL;
+ final int oldRule = mUidRules.get(uid);
- if (!uidForeground) {
- // If the app is not in foreground, reject access if:
- // - app is blacklisted by policy or
- // - data saver mode is and app is not whitelisted
- if (((uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0)
- || (mRestrictBackground && !mRestrictBackgroundWhitelistUids.get(uid))) {
+ // First step: define the new rule based on user restrictions and foreground state.
+ if (isForeground) {
+ if (isBlacklisted || (mRestrictBackground && !isWhitelisted)) {
+ newRule = RULE_TEMPORARY_ALLOW_METERED;
+ }
+ } else {
+ if (isBlacklisted) {
newRule = RULE_REJECT_METERED;
+ } else if (isWhitelisted) {
+ newRule = RULE_ALLOW_METERED;
}
}
- final int oldRule = mUidRules.get(uid);
if (LOGV) {
- Log.v(TAG, "updateRulesForRestrictBackgroundLocked(" + uid + "): oldRule = "
- + ruleToString(oldRule) + ", newRule = " + ruleToString(newRule));
+ Log.v(TAG, "updateRuleForRestrictBackgroundLocked(" + uid + "):"
+ + " isForeground=" +isForeground + ", isBlacklisted: " + isBlacklisted
+ + ", isWhitelisted: " + isWhitelisted + ", newRule: " + ruleToString(newRule)
+ + ", oldRule: " + ruleToString(oldRule));
}
+
if (newRule == RULE_ALLOW_ALL) {
mUidRules.delete(uid);
} else {
mUidRules.put(uid, newRule);
}
- if (oldRule != newRule) {
- final boolean rejectMetered = (newRule == RULE_REJECT_METERED);
- setUidNetworkRules(uid, rejectMetered);
+ // Second step: apply bw changes based on change of state.
+ if (newRule != oldRule) {
+ if (newRule == RULE_TEMPORARY_ALLOW_METERED) {
+ // Temporarily whitelist foreground app, removing from blacklist if necessary
+ // (since bw_penalty_box prevails over bw_happy_box).
+
+ setMeteredNetworkWhitelist(uid, true);
+ // TODO: if statement below is used to avoid an unnecessary call to netd / iptables,
+ // but ideally it should be just:
+ // setMeteredNetworkBlacklist(uid, isBlacklisted);
+ if (isBlacklisted) {
+ setMeteredNetworkBlacklist(uid, false);
+ }
+ } else if (oldRule == RULE_TEMPORARY_ALLOW_METERED) {
+ // Remove temporary whitelist from app that is not on foreground anymore.
+
+ // TODO: if statements below are used to avoid unnecessary calls to netd / iptables,
+ // but ideally they should be just:
+ // setMeteredNetworkWhitelist(uid, isWhitelisted);
+ // setMeteredNetworkBlacklist(uid, isBlacklisted);
+ if (!isWhitelisted) {
+ setMeteredNetworkWhitelist(uid, false);
+ }
+ if (isBlacklisted) {
+ setMeteredNetworkBlacklist(uid, true);
+ }
+ } else if (newRule == RULE_REJECT_METERED || oldRule == RULE_REJECT_METERED) {
+ // Flip state because app was explicitly added or removed to blacklist.
+ setMeteredNetworkBlacklist(uid, isBlacklisted);
+ if (oldRule == RULE_REJECT_METERED && isWhitelisted) {
+ // Since blacklist prevails over whitelist, we need to handle the special case
+ // where app is whitelisted and blacklisted at the same time (although such
+ // scenario should be blocked by the UI), then blacklist is removed.
+ setMeteredNetworkWhitelist(uid, isWhitelisted);
+ }
+ } else if (newRule == RULE_ALLOW_METERED || oldRule == RULE_ALLOW_METERED) {
+ // Flip state because app was explicitly added or removed to whitelist.
+ setMeteredNetworkWhitelist(uid, isWhitelisted);
+ } else {
+ // All scenarios should have been covered above
+ Log.wtf(TAG, "Unexpected change of state for " + uid
+ + ": foreground=" + isForeground + ", whitelisted=" + isWhitelisted
+ + ", blacklisted=" + isBlacklisted + ", newRule="
+ + ruleToString(newRule) + ", oldRule=" + ruleToString(oldRule));
+ }
// dispatch changed rule to existing listeners
mHandler.obtainMessage(MSG_RULES_CHANGED, uid, newRule).sendToTarget();
}
-
}
private class AppIdleStateChangeListener
@@ -2825,11 +2974,23 @@
}
}
- private void setUidNetworkRules(int uid, boolean rejectOnQuotaInterfaces) {
+ private void setMeteredNetworkBlacklist(int uid, boolean enable) {
+ if (LOGV) Slog.v(TAG, "setMeteredNetworkBlacklist " + uid + ": " + enable);
try {
- mNetworkManager.setUidMeteredNetworkBlacklist(uid, rejectOnQuotaInterfaces);
+ mNetworkManager.setUidMeteredNetworkBlacklist(uid, enable);
} catch (IllegalStateException e) {
- Log.wtf(TAG, "problem setting uid rules", e);
+ Log.wtf(TAG, "problem setting blacklist (" + enable + ") rules for " + uid, e);
+ } catch (RemoteException e) {
+ // ignored; service lives in system_server
+ }
+ }
+
+ private void setMeteredNetworkWhitelist(int uid, boolean enable) {
+ if (LOGV) Slog.v(TAG, "setMeteredNetworkWhitelist " + uid + ": " + enable);
+ try {
+ mNetworkManager.setUidMeteredNetworkWhitelist(uid, enable);
+ } catch (IllegalStateException e) {
+ Log.wtf(TAG, "problem setting whitelist (" + enable + ") rules for " + uid, e);
} catch (RemoteException e) {
// ignored; service lives in system_server
}
@@ -3011,7 +3172,7 @@
public void onPackageRemoved(String packageName, int uid) {
if (LOGV) Slog.v(TAG, "onPackageRemoved: " + packageName + " ->" + uid);
synchronized (mRulesLock) {
- removeRestrictBackgroundWhitelistedUidLocked(uid, true);
+ removeRestrictBackgroundWhitelistedUidLocked(uid, true, true);
}
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 0f23fde..e9d721c 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1852,10 +1852,16 @@
}
private boolean checkPolicyAccess(String pkg) {
- if (PackageManager.PERMISSION_GRANTED == ActivityManager.checkComponentPermission(
- android.Manifest.permission.MANAGE_NOTIFICATIONS, Binder.getCallingUid(),
- -1, true)) {
- return true;
+ try {
+ int uid = getContext().getPackageManager().getPackageUidAsUser(
+ pkg, UserHandle.getCallingUserId());
+ if (PackageManager.PERMISSION_GRANTED == ActivityManager.checkComponentPermission(
+ android.Manifest.permission.MANAGE_NOTIFICATIONS, uid,
+ -1, true)) {
+ return true;
+ }
+ } catch (NameNotFoundException e) {
+ return false;
}
return checkPackagePolicyAccess(pkg) || mListeners.isComponentEnabledForPackage(pkg);
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index fdbbd85..926ff37 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -6962,10 +6962,11 @@
// If the cache was pruned, any compiled odex files will likely be out of date
// and would have to be patched (would be SELF_PATCHOAT, which is deprecated).
// Instead, force the extraction in this case.
- performDexOpt(pkg.packageName, null /* instructionSet */,
- false /* checkProfiles */,
- causeFirstBoot ? REASON_FIRST_BOOT : REASON_BOOT,
- causePrunedCache);
+ performDexOpt(pkg.packageName,
+ null /* instructionSet */,
+ false /* checkProfiles */,
+ causeFirstBoot ? REASON_FIRST_BOOT : REASON_BOOT,
+ false /* force */);
}
}
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 574faa0..747e31e 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -18,7 +18,6 @@
import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
-import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
import static android.app.ActivityManager.StackId.HOME_STACK_ID;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static android.content.pm.PackageManager.FEATURE_TELEVISION;
@@ -38,7 +37,6 @@
import static android.view.WindowManagerPolicy.WindowManagerFuncs.LID_CLOSED;
import static android.view.WindowManagerPolicy.WindowManagerFuncs.LID_OPEN;
-import android.Manifest;
import android.app.ActivityManager;
import android.app.ActivityManager.StackId;
import android.app.ActivityManagerInternal;
@@ -4647,7 +4645,7 @@
// TYPE_SYSTEM_ERROR is above the NavigationBar so it can't be allowed to extend over it.
// Also, we don't allow windows in multi-window mode to extend out of the screen.
if ((fl & FLAG_LAYOUT_NO_LIMITS) != 0 && attrs.type != TYPE_SYSTEM_ERROR
- && !win.inMultiWindowMode()) {
+ && !win.isInMultiWindowMode()) {
df.left = df.top = -10000;
df.right = df.bottom = 10000;
if (attrs.type != TYPE_WALLPAPER) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 1a9e206..97c4c42 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -2602,8 +2602,6 @@
== PackageManager.PERMISSION_GRANTED;
long origId = Binder.clearCallingIdentity();
- final boolean preserveGeometry = (attrs != null) && (attrs.privateFlags &
- WindowManager.LayoutParams.PRIVATE_FLAG_PRESERVE_GEOMETRY) != 0;
synchronized(mWindowMap) {
WindowState win = windowForClientLocked(session, client, false);
if (win == null) {
@@ -2611,7 +2609,7 @@
}
WindowStateAnimator winAnimator = win.mWinAnimator;
- if (!preserveGeometry && viewVisibility != View.GONE) {
+ if (viewVisibility != View.GONE) {
win.setRequestedSize(requestedWidth, requestedHeight);
}
@@ -2660,9 +2658,7 @@
if ((attrChanges & WindowManager.LayoutParams.ALPHA_CHANGED) != 0) {
winAnimator.mAlpha = attrs.alpha;
}
- if (!preserveGeometry) {
- win.setWindowScale(win.mRequestedWidth, win.mRequestedHeight);
- }
+ win.setWindowScale(win.mRequestedWidth, win.mRequestedHeight);
boolean imMayMove = (flagChanges & (FLAG_ALT_FOCUSABLE_IM | FLAG_NOT_FOCUSABLE)) != 0;
final boolean isDefaultDisplay = win.isDefaultDisplay();
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 0866c03..57ead8b 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -642,7 +642,7 @@
mHaveFrame = true;
final Task task = getTask();
- final boolean fullscreenTask = !inMultiWindowMode();
+ final boolean fullscreenTask = !isInMultiWindowMode();
final boolean windowsAreFloating = task != null && task.isFloating();
// If the task has temp inset bounds set, we have to make sure all its windows uses
@@ -2226,7 +2226,7 @@
}
@Override
- public boolean inMultiWindowMode() {
+ public boolean isInMultiWindowMode() {
final Task task = getTask();
return task != null && !task.isFullscreen();
}
@@ -2527,7 +2527,7 @@
final int pw = mContainingFrame.width();
final int ph = mContainingFrame.height();
final Task task = getTask();
- final boolean nonFullscreenTask = inMultiWindowMode();
+ final boolean nonFullscreenTask = isInMultiWindowMode();
final boolean fitToDisplay = task != null && !task.isFloating() && !layoutInParentFrame();
float x, y;
int w,h;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 3e368f5..96ec02a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -5796,8 +5796,7 @@
transitionCheckNeeded = false;
} else {
// For all other cases, caller must have MANAGE_PROFILE_AND_DEVICE_OWNERS.
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, null);
+ enforceCanManageProfileAndDeviceOwners();
}
final DevicePolicyData policyData = getUserData(userHandle);
@@ -5990,8 +5989,7 @@
}
return;
}
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, null);
+ enforceCanManageProfileAndDeviceOwners();
if (hasUserSetupCompleted(userHandle) && !isCallerWithSystemUid()) {
throw new IllegalStateException("Cannot set the profile owner on a user which is "
+ "already set-up");
@@ -6006,8 +6004,7 @@
int callingUid = mInjector.binderGetCallingUid();
boolean isAdb = callingUid == Process.SHELL_UID || callingUid == Process.ROOT_UID;
if (!isAdb) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, null);
+ enforceCanManageProfileAndDeviceOwners();
}
final int code = checkSetDeviceOwnerPreCondition(userId, isAdb);
@@ -6663,6 +6660,9 @@
}
synchronized (this) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
+ if (admin == null) {
+ return false;
+ }
if (admin.permittedAccessiblityServices == null) {
return true;
}
@@ -6833,6 +6833,9 @@
}
synchronized (this) {
ActiveAdmin admin = getActiveAdminUncheckedLocked(who, userHandle);
+ if (admin == null) {
+ return false;
+ }
if (admin.permittedInputMethods == null) {
return true;
}
@@ -7103,19 +7106,30 @@
}
@Override
- public Bundle getUserRestrictions(ComponentName who, int userHandle) {
+ public Bundle getUserRestrictions(ComponentName who) {
+ if (!mHasFeature) {
+ return null;
+ }
+ Preconditions.checkNotNull(who, "ComponentName is null");
+ synchronized (this) {
+ final ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(who,
+ DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ return activeAdmin.userRestrictions;
+ }
+ }
+
+ @Override
+ public Bundle getUserRestrictionsForUser(ComponentName who, int userHandle) {
+ if (!mHasFeature) {
+ return null;
+ }
Preconditions.checkNotNull(who, "ComponentName is null");
enforceFullCrossUsersPermission(userHandle);
+ enforceCanManageProfileAndDeviceOwners();
synchronized (this) {
ActiveAdmin activeAdmin = getActiveAdminUncheckedLocked(who, userHandle);
if (activeAdmin == null) {
- throw new SecurityException("No active admin: " + activeAdmin);
- }
- if (activeAdmin.getUid() != mInjector.binderGetCallingUid()) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS,
- "Calling uid " + mInjector.binderGetCallingUid() + " neither owns the admin"
- + " " + who + " nor has MANAGE_PROFILE_AND_DEVICE_OWNERS permission");
+ return null;
}
return activeAdmin.userRestrictions;
}
@@ -8689,6 +8703,11 @@
null);
}
+ private void enforceCanManageProfileAndDeviceOwners() {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS, null);
+ }
+
@Override
public boolean isUninstallInQueue(final String packageName) {
enforceCanManageDeviceAdmin();
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 7b44f98..b026bc5 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1132,7 +1132,9 @@
mSystemServiceManager.startService(TvInputManagerService.class);
}
- mSystemServiceManager.startService(MediaResourceMonitorService.class);
+ if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
+ mSystemServiceManager.startService(MediaResourceMonitorService.class);
+ }
if (!disableNonCoreServices) {
traceBeginAndSlog("StartMediaRouterService");
diff --git a/services/print/java/com/android/server/print/PrintManagerService.java b/services/print/java/com/android/server/print/PrintManagerService.java
index 985917b..4d02928 100644
--- a/services/print/java/com/android/server/print/PrintManagerService.java
+++ b/services/print/java/com/android/server/print/PrintManagerService.java
@@ -41,12 +41,14 @@
import android.print.IPrintDocumentAdapter;
import android.print.IPrintJobStateChangeListener;
import android.print.IPrintManager;
+import android.printservice.recommendation.IRecommendationsChangeListener;
import android.print.IPrintServicesChangeListener;
import android.print.IPrinterDiscoveryObserver;
import android.print.PrintAttributes;
import android.print.PrintJobId;
import android.print.PrintJobInfo;
import android.print.PrintManager;
+import android.printservice.recommendation.RecommendationInfo;
import android.print.PrinterId;
import android.printservice.PrintServiceInfo;
import android.provider.Settings;
@@ -265,7 +267,7 @@
final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
final UserState userState;
synchronized (mLock) {
- // Only the current group members can get enabled services.
+ // Only the current group members can get print services.
if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
return null;
}
@@ -314,6 +316,25 @@
}
@Override
+ public List<RecommendationInfo> getPrintServiceRecommendations(int userId) {
+ final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
+ final UserState userState;
+ synchronized (mLock) {
+ // Only the current group members can get print service recommendations.
+ if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
+ return null;
+ }
+ userState = getOrCreateUserStateLocked(resolvedUserId, false);
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return userState.getPrintServiceRecommendations();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void createPrinterDiscoverySession(IPrinterDiscoveryObserver observer,
int userId) {
observer = Preconditions.checkNotNull(observer);
@@ -543,7 +564,7 @@
final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
final UserState userState;
synchronized (mLock) {
- // Only the current group members can remove a print job listener.
+ // Only the current group members can remove a print services change listener.
if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
return;
}
@@ -558,6 +579,52 @@
}
@Override
+ public void addPrintServiceRecommendationsChangeListener(
+ IRecommendationsChangeListener listener, int userId)
+ throws RemoteException {
+ listener = Preconditions.checkNotNull(listener);
+
+ final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
+ final UserState userState;
+ synchronized (mLock) {
+ // Only the current group members can add a print service recommendations listener.
+ if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
+ return;
+ }
+ userState = getOrCreateUserStateLocked(resolvedUserId, false);
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ userState.addPrintServiceRecommendationsChangeListener(listener);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void removePrintServiceRecommendationsChangeListener(
+ IRecommendationsChangeListener listener, int userId) {
+ listener = Preconditions.checkNotNull(listener);
+
+ final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
+ final UserState userState;
+ synchronized (mLock) {
+ // Only the current group members can remove a print service recommendations
+ // listener.
+ if (resolveCallingProfileParentLocked(resolvedUserId) != getCurrentUserId()) {
+ return;
+ }
+ userState = getOrCreateUserStateLocked(resolvedUserId, false);
+ }
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ userState.removePrintServiceRecommendationsChangeListener(listener);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
fd = Preconditions.checkNotNull(fd);
pw = Preconditions.checkNotNull(pw);
diff --git a/services/print/java/com/android/server/print/RemotePrintServiceRecommendationService.java b/services/print/java/com/android/server/print/RemotePrintServiceRecommendationService.java
new file mode 100644
index 0000000..fa1f232
--- /dev/null
+++ b/services/print/java/com/android/server/print/RemotePrintServiceRecommendationService.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2016 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.print;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.printservice.recommendation.IRecommendationService;
+import android.printservice.recommendation.IRecommendationServiceCallbacks;
+import android.printservice.recommendation.RecommendationInfo;
+import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+import static android.content.pm.PackageManager.GET_META_DATA;
+import static android.content.pm.PackageManager.GET_SERVICES;
+import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
+
+/**
+ * Connection to a remote print service recommendation service.
+ */
+class RemotePrintServiceRecommendationService {
+ private static final String LOG_TAG = "RemotePrintServiceRecS";
+
+ /** Lock for this object */
+ private final Object mLock = new Object();
+
+ /** Context used for the connection */
+ private @NonNull final Context mContext;
+
+ /** The connection to the service (if {@link #mIsBound bound}) */
+ @GuardedBy("mLock")
+ private @NonNull final Connection mConnection;
+
+ /** If the service is currently bound. */
+ @GuardedBy("mLock")
+ private boolean mIsBound;
+
+ /** The service once bound */
+ @GuardedBy("mLock")
+ private IRecommendationService mService;
+
+ /**
+ * Callbacks to be called when there are updates to the print service recommendations.
+ */
+ public interface RemotePrintServiceRecommendationServiceCallbacks {
+ /**
+ * Called when there is an update list of print service recommendations.
+ *
+ * @param recommendations The new recommendations.
+ */
+ void onPrintServiceRecommendationsUpdated(
+ @Nullable List<RecommendationInfo> recommendations);
+ }
+
+ /**
+ * @return The intent that is used to connect to the print service recommendation service.
+ */
+ private Intent getServiceIntent(@NonNull UserHandle userHandle) throws Exception {
+ List<ResolveInfo> installedServices = mContext.getPackageManager()
+ .queryIntentServicesAsUser(new Intent(
+ android.printservice.recommendation.RecommendationService.SERVICE_INTERFACE),
+ GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING,
+ userHandle.getIdentifier());
+
+ if (installedServices.size() != 1) {
+ throw new Exception(installedServices.size() + " instead of exactly one service found");
+ }
+
+ ResolveInfo installedService = installedServices.get(0);
+
+ ComponentName serviceName = new ComponentName(
+ installedService.serviceInfo.packageName,
+ installedService.serviceInfo.name);
+
+ ApplicationInfo appInfo = mContext.getPackageManager()
+ .getApplicationInfo(installedService.serviceInfo.packageName, 0);
+
+ if (appInfo == null) {
+ throw new Exception("Cannot read appInfo for service");
+ }
+
+ if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
+ throw new Exception("Service is not part of the system");
+ }
+
+ if (!android.Manifest.permission.BIND_PRINT_RECOMMENDATION_SERVICE.equals(
+ installedService.serviceInfo.permission)) {
+ throw new Exception("Service " + serviceName.flattenToShortString()
+ + " does not require permission "
+ + android.Manifest.permission.BIND_PRINT_RECOMMENDATION_SERVICE);
+ }
+
+ Intent serviceIntent = new Intent();
+ serviceIntent.setComponent(serviceName);
+
+ return serviceIntent;
+ }
+
+ /**
+ * Open a new connection to a {@link IRecommendationService remote print service
+ * recommendation service}.
+ *
+ * @param context The context establishing the connection
+ * @param userHandle The user the connection is for
+ * @param callbacks The callbacks to call by the service
+ */
+ RemotePrintServiceRecommendationService(@NonNull Context context,
+ @NonNull UserHandle userHandle,
+ @NonNull RemotePrintServiceRecommendationServiceCallbacks callbacks) {
+ mContext = context;
+ mConnection = new Connection(callbacks);
+
+ try {
+ Intent serviceIntent = getServiceIntent(userHandle);
+
+ synchronized (mLock) {
+ mIsBound = mContext.bindServiceAsUser(serviceIntent, mConnection,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, userHandle);
+
+ if (!mIsBound) {
+ throw new Exception("Failed to bind to service " + serviceIntent);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Could not connect to print service recommendation service", e);
+ }
+ }
+
+ /**
+ * Terminate the connection to the {@link IRecommendationService remote print
+ * service recommendation service}.
+ */
+ void close() {
+ synchronized (mLock) {
+ if (mService != null) {
+ try {
+ mService.registerCallbacks(null);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Could not unregister callbacks", e);
+ }
+
+ mService = null;
+ }
+
+ if (mIsBound) {
+ mContext.unbindService(mConnection);
+ mIsBound = false;
+ }
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (mIsBound || mService != null) {
+ Log.w(LOG_TAG, "Service still connected on finalize()");
+ close();
+ }
+
+ super.finalize();
+ }
+
+ /**
+ * Connection to the service.
+ */
+ private class Connection implements ServiceConnection {
+ private final RemotePrintServiceRecommendationServiceCallbacks mCallbacks;
+
+ public Connection(@NonNull RemotePrintServiceRecommendationServiceCallbacks callbacks) {
+ mCallbacks = callbacks;
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (mLock) {
+ mService = (IRecommendationService)IRecommendationService.Stub.asInterface(service);
+
+ try {
+ mService.registerCallbacks(new IRecommendationServiceCallbacks.Stub() {
+ @Override
+ public void onRecommendationsUpdated(
+ List<RecommendationInfo> recommendations) {
+ synchronized (mLock) {
+ if (mIsBound && mService != null) {
+ if (recommendations != null) {
+ Preconditions.checkCollectionElementsNotNull(
+ recommendations, "recommendation");
+ }
+
+ mCallbacks.onPrintServiceRecommendationsUpdated(
+ recommendations);
+ }
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Could not register callbacks", e);
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.w(LOG_TAG, "Unexpected termination of connection");
+
+ synchronized (mLock) {
+ mService = null;
+ }
+ }
+ }
+}
diff --git a/services/print/java/com/android/server/print/UserState.java b/services/print/java/com/android/server/print/UserState.java
index 263dead..026942e 100644
--- a/services/print/java/com/android/server/print/UserState.java
+++ b/services/print/java/com/android/server/print/UserState.java
@@ -37,6 +37,7 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
+import android.os.IInterface;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteCallbackList;
@@ -44,12 +45,14 @@
import android.os.UserHandle;
import android.print.IPrintDocumentAdapter;
import android.print.IPrintJobStateChangeListener;
+import android.printservice.recommendation.IRecommendationsChangeListener;
import android.print.IPrintServicesChangeListener;
import android.print.IPrinterDiscoveryObserver;
import android.print.PrintAttributes;
import android.print.PrintJobId;
import android.print.PrintJobInfo;
import android.print.PrintManager;
+import android.printservice.recommendation.RecommendationInfo;
import android.print.PrinterId;
import android.print.PrinterInfo;
import android.printservice.PrintServiceInfo;
@@ -68,6 +71,7 @@
import com.android.internal.os.SomeArgs;
import com.android.server.print.RemotePrintService.PrintServiceCallbacks;
import com.android.server.print.RemotePrintSpooler.PrintSpoolerCallbacks;
+import com.android.server.print.RemotePrintServiceRecommendationService.RemotePrintServiceRecommendationServiceCallbacks;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -82,7 +86,8 @@
/**
* Represents the print state for a user.
*/
-final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks {
+final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
+ RemotePrintServiceRecommendationServiceCallbacks {
private static final String LOG_TAG = "UserState";
@@ -122,10 +127,22 @@
private List<PrintJobStateChangeListenerRecord> mPrintJobStateChangeListenerRecords;
- private List<PrintServicesChangeListenerRecord> mPrintServicesChangeListenerRecords;
+ private List<ListenerRecord<IPrintServicesChangeListener>> mPrintServicesChangeListenerRecords;
+
+ private List<ListenerRecord<IRecommendationsChangeListener>>
+ mPrintServiceRecommendationsChangeListenerRecords;
private boolean mDestroyed;
+ /** Currently known list of print service recommendations */
+ private List<RecommendationInfo> mPrintServiceRecommendations;
+
+ /**
+ * Connection to the service updating the {@link #mPrintServiceRecommendations print service
+ * recommendations}.
+ */
+ private RemotePrintServiceRecommendationService mPrintServiceRecommendationsService;
+
public UserState(Context context, int userId, Object lock, boolean lowPriority) {
mContext = context;
mUserId = userId;
@@ -409,6 +426,13 @@
}
}
+ /**
+ * @return The currently known print service recommendations
+ */
+ public @Nullable List<RecommendationInfo> getPrintServiceRecommendations() {
+ return mPrintServiceRecommendations;
+ }
+
public void createPrinterDiscoverySession(@NonNull IPrinterDiscoveryObserver observer) {
synchronized (mLock) {
throwIfDestroyedLocked();
@@ -566,7 +590,7 @@
mPrintServicesChangeListenerRecords = new ArrayList<>();
}
mPrintServicesChangeListenerRecords.add(
- new PrintServicesChangeListenerRecord(listener) {
+ new ListenerRecord<IPrintServicesChangeListener>(listener) {
@Override
public void onBinderDied() {
mPrintServicesChangeListenerRecords.remove(this);
@@ -583,7 +607,7 @@
}
final int recordCount = mPrintServicesChangeListenerRecords.size();
for (int i = 0; i < recordCount; i++) {
- PrintServicesChangeListenerRecord record =
+ ListenerRecord<IPrintServicesChangeListener> record =
mPrintServicesChangeListenerRecords.get(i);
if (record.listener.asBinder().equals(listener.asBinder())) {
mPrintServicesChangeListenerRecords.remove(i);
@@ -596,6 +620,54 @@
}
}
+ public void addPrintServiceRecommendationsChangeListener(
+ @NonNull IRecommendationsChangeListener listener) throws RemoteException {
+ synchronized (mLock) {
+ throwIfDestroyedLocked();
+ if (mPrintServiceRecommendationsChangeListenerRecords == null) {
+ mPrintServiceRecommendationsChangeListenerRecords = new ArrayList<>();
+
+ mPrintServiceRecommendationsService =
+ new RemotePrintServiceRecommendationService(mContext,
+ UserHandle.getUserHandleForUid(mUserId), this);
+ }
+ mPrintServiceRecommendationsChangeListenerRecords.add(
+ new ListenerRecord<IRecommendationsChangeListener>(listener) {
+ @Override
+ public void onBinderDied() {
+ mPrintServiceRecommendationsChangeListenerRecords.remove(this);
+ }
+ });
+ }
+ }
+
+ public void removePrintServiceRecommendationsChangeListener(
+ @NonNull IRecommendationsChangeListener listener) {
+ synchronized (mLock) {
+ throwIfDestroyedLocked();
+ if (mPrintServiceRecommendationsChangeListenerRecords == null) {
+ return;
+ }
+ final int recordCount = mPrintServiceRecommendationsChangeListenerRecords.size();
+ for (int i = 0; i < recordCount; i++) {
+ ListenerRecord<IRecommendationsChangeListener> record =
+ mPrintServiceRecommendationsChangeListenerRecords.get(i);
+ if (record.listener.asBinder().equals(listener.asBinder())) {
+ mPrintServiceRecommendationsChangeListenerRecords.remove(i);
+ break;
+ }
+ }
+ if (mPrintServiceRecommendationsChangeListenerRecords.isEmpty()) {
+ mPrintServiceRecommendationsChangeListenerRecords = null;
+
+ mPrintServiceRecommendations = null;
+
+ mPrintServiceRecommendationsService.close();
+ mPrintServiceRecommendationsService = null;
+ }
+ }
+ }
+
@Override
public void onPrintJobStateChanged(PrintJobInfo printJob) {
mPrintJobForAppCache.onPrintJobStateChanged(printJob);
@@ -608,6 +680,12 @@
}
@Override
+ public void onPrintServiceRecommendationsUpdated(List<RecommendationInfo> recommendations) {
+ mHandler.obtainMessage(UserStateHandler.MSG_DISPATCH_PRINT_SERVICES_RECOMMENDATIONS_UPDATED,
+ 0, 0, recommendations).sendToTarget();
+ }
+
+ @Override
public void onPrintersAdded(List<PrinterInfo> printers) {
synchronized (mLock) {
throwIfDestroyedLocked();
@@ -1058,7 +1136,7 @@
}
private void handleDispatchPrintServicesChanged() {
- final List<PrintServicesChangeListenerRecord> records;
+ final List<ListenerRecord<IPrintServicesChangeListener>> records;
synchronized (mLock) {
if (mPrintServicesChangeListenerRecords == null) {
return;
@@ -1067,7 +1145,7 @@
}
final int recordCount = records.size();
for (int i = 0; i < recordCount; i++) {
- PrintServicesChangeListenerRecord record = records.get(i);
+ ListenerRecord<IPrintServicesChangeListener> record = records.get(i);
try {
record.listener.onPrintServicesChanged();;
@@ -1077,9 +1155,33 @@
}
}
+ private void handleDispatchPrintServiceRecommendationsUpdated(
+ @Nullable List<RecommendationInfo> recommendations) {
+ final List<ListenerRecord<IRecommendationsChangeListener>> records;
+ synchronized (mLock) {
+ if (mPrintServiceRecommendationsChangeListenerRecords == null) {
+ return;
+ }
+ records = new ArrayList<>(mPrintServiceRecommendationsChangeListenerRecords);
+
+ mPrintServiceRecommendations = recommendations;
+ }
+ final int recordCount = records.size();
+ for (int i = 0; i < recordCount; i++) {
+ ListenerRecord<IRecommendationsChangeListener> record = records.get(i);
+
+ try {
+ record.listener.onRecommendationsChanged();
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error notifying for print service recommendations change", re);
+ }
+ }
+ }
+
private final class UserStateHandler extends Handler {
public static final int MSG_DISPATCH_PRINT_JOB_STATE_CHANGED = 1;
public static final int MSG_DISPATCH_PRINT_SERVICES_CHANGED = 2;
+ public static final int MSG_DISPATCH_PRINT_SERVICES_RECOMMENDATIONS_UPDATED = 3;
public UserStateHandler(Looper looper) {
super(looper, null, false);
@@ -1096,6 +1198,10 @@
case MSG_DISPATCH_PRINT_SERVICES_CHANGED:
handleDispatchPrintServicesChanged();
break;
+ case MSG_DISPATCH_PRINT_SERVICES_RECOMMENDATIONS_UPDATED:
+ handleDispatchPrintServiceRecommendationsUpdated(
+ (List<RecommendationInfo>) message.obj);
+ break;
default:
// not reached
}
@@ -1122,10 +1228,10 @@
public abstract void onBinderDied();
}
- private abstract class PrintServicesChangeListenerRecord implements DeathRecipient {
- @NonNull final IPrintServicesChangeListener listener;
+ private abstract class ListenerRecord<T extends IInterface> implements DeathRecipient {
+ @NonNull final T listener;
- public PrintServicesChangeListenerRecord(@NonNull IPrintServicesChangeListener listener) throws RemoteException {
+ public ListenerRecord(@NonNull T listener) throws RemoteException {
this.listener = listener;
listener.asBinder().linkToDeath(this, 0);
}
diff --git a/tests/LocationTracker/src/com/android/locationtracker/SettingsActivity.java b/tests/LocationTracker/src/com/android/locationtracker/SettingsActivity.java
index cb77118..a169b18 100644
--- a/tests/LocationTracker/src/com/android/locationtracker/SettingsActivity.java
+++ b/tests/LocationTracker/src/com/android/locationtracker/SettingsActivity.java
@@ -1,35 +1,35 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.locationtracker;
-
-import android.os.Bundle;
-import android.preference.PreferenceActivity;
-
-/**
- * Settings preference screen for location tracker
- */
-public class SettingsActivity extends PreferenceActivity {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- // Load the preferences from an XML resource
- addPreferencesFromResource(R.xml.preferences);
- }
-
-}
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.locationtracker;
+
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+/**
+ * Settings preference screen for location tracker
+ */
+public class SettingsActivity extends PreferenceActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.preferences);
+ }
+
+}
diff --git a/tests/LocationTracker/src/com/android/locationtracker/data/TrackerListHelper.java b/tests/LocationTracker/src/com/android/locationtracker/data/TrackerListHelper.java
index 55d4d1e..adc39b3 100644
--- a/tests/LocationTracker/src/com/android/locationtracker/data/TrackerListHelper.java
+++ b/tests/LocationTracker/src/com/android/locationtracker/data/TrackerListHelper.java
@@ -1,75 +1,75 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.android.locationtracker.data;
-
-import android.app.ListActivity;
-import android.content.Context;
-import android.database.Cursor;
-import android.view.View;
-import android.widget.ResourceCursorAdapter;
-import android.widget.TextView;
-
-import com.android.locationtracker.R;
-
-/**
- * Used to bind Tracker data to a list view UI
- */
-public class TrackerListHelper extends TrackerDataHelper {
-
- private ListActivity mActivity;
-
- // sort entries by most recent first
- private static final String SORT_ORDER = TrackerEntry.ID_COL + " DESC";
-
- public TrackerListHelper(ListActivity activity) {
- super(activity, TrackerDataHelper.CSV_FORMATTER);
- mActivity = activity;
- }
-
- /**
- * Helper method for binding the list activities UI to the tracker data
- * Tracker data will be sorted in most-recent first order
- * Will enable automatic UI changes as tracker data changes
- *
- * @param layout - layout to populate data
- */
- public void bindListUI(int layout) {
- Cursor cursor = mActivity.managedQuery(TrackerProvider.CONTENT_URI,
- TrackerEntry.ATTRIBUTES, null, null, SORT_ORDER);
- // Used to map tracker entries from the database to views
- TrackerAdapter adapter = new TrackerAdapter(mActivity, layout, cursor);
- mActivity.setListAdapter(adapter);
- cursor.setNotificationUri(mActivity.getContentResolver(),
- TrackerProvider.CONTENT_URI);
-
- }
-
- private class TrackerAdapter extends ResourceCursorAdapter {
-
- public TrackerAdapter(Context context, int layout, Cursor c) {
- super(context, layout, c);
- }
-
- @Override
- public void bindView(View view, Context context, Cursor cursor) {
- final TextView v = (TextView) view
- .findViewById(R.id.entrylist_item);
- String rowText = mFormatter.getOutput(TrackerEntry
- .createEntry(cursor));
- v.setText(rowText);
- }
- }
-}
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.locationtracker.data;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+import com.android.locationtracker.R;
+
+/**
+ * Used to bind Tracker data to a list view UI
+ */
+public class TrackerListHelper extends TrackerDataHelper {
+
+ private ListActivity mActivity;
+
+ // sort entries by most recent first
+ private static final String SORT_ORDER = TrackerEntry.ID_COL + " DESC";
+
+ public TrackerListHelper(ListActivity activity) {
+ super(activity, TrackerDataHelper.CSV_FORMATTER);
+ mActivity = activity;
+ }
+
+ /**
+ * Helper method for binding the list activities UI to the tracker data
+ * Tracker data will be sorted in most-recent first order
+ * Will enable automatic UI changes as tracker data changes
+ *
+ * @param layout - layout to populate data
+ */
+ public void bindListUI(int layout) {
+ Cursor cursor = mActivity.managedQuery(TrackerProvider.CONTENT_URI,
+ TrackerEntry.ATTRIBUTES, null, null, SORT_ORDER);
+ // Used to map tracker entries from the database to views
+ TrackerAdapter adapter = new TrackerAdapter(mActivity, layout, cursor);
+ mActivity.setListAdapter(adapter);
+ cursor.setNotificationUri(mActivity.getContentResolver(),
+ TrackerProvider.CONTENT_URI);
+
+ }
+
+ private class TrackerAdapter extends ResourceCursorAdapter {
+
+ public TrackerAdapter(Context context, int layout, Cursor c) {
+ super(context, layout, c);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ final TextView v = (TextView) view
+ .findViewById(R.id.entrylist_item);
+ String rowText = mFormatter.getOutput(TrackerEntry
+ .createEntry(cursor));
+ v.setText(rowText);
+ }
+ }
+}
diff --git a/tests/VectorDrawableTest/res/drawable/btn_radio_on_to_off_bundle.xml b/tests/VectorDrawableTest/res/drawable/btn_radio_on_to_off_bundle.xml
new file mode 100644
index 0000000..4f05090
--- /dev/null
+++ b/tests/VectorDrawableTest/res/drawable/btn_radio_on_to_off_bundle.xml
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<animated-vector xmlns:aapt="http://schemas.android.com/aapt"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <aapt:attr name="android:drawable">
+ <vector
+ android:width="32dp"
+ android:viewportWidth="32"
+ android:height="32dp"
+ android:viewportHeight="32">
+ <group
+ android:name="btn_radio_to_off_mtrl_0"
+ android:translateX="16"
+ android:translateY="16">
+ <group
+ android:name="ring_outer">
+ <path
+ android:name="ring_outer_path"
+ android:strokeColor="#FF000000"
+ android:strokeWidth="2"
+ android:pathData="M 0.0,-9.0 c 4.9705627482,0.0 9.0,4.0294372518 9.0,9.0 c 0.0,4.9705627482 -4.0294372518,9.0 -9.0,9.0 c -4.9705627482,0.0 -9.0,-4.0294372518 -9.0,-9.0 c 0.0,-4.9705627482 4.0294372518,-9.0 9.0,-9.0 Z"/>
+ </group>
+ <group
+ android:name="dot_group">
+ <path
+ android:name="dot_path"
+ android:pathData="M 0.0,-5.0 c -2.7619934082,0.0 -5.0,2.2380065918 -5.0,5.0 c 0.0,2.7619934082 2.2380065918,5.0 5.0,5.0 c 2.7619934082,0.0 5.0,-2.2380065918 5.0,-5.0 c 0.0,-2.7619934082 -2.2380065918,-5.0 -5.0,-5.0 Z"
+ android:fillColor="#FF000000"/>
+ </group>
+ </group>
+ </vector>
+ </aapt:attr>
+ <target android:name="ring_outer">
+ <aapt:attr name="android:animation">
+ <set
+ xmlns:android="http://schemas.android.com/apk/res/android" >
+ <set
+ android:ordering="sequentially" >
+ <objectAnimator
+ android:duration="183"
+ android:propertyName="scaleX"
+ android:valueFrom="1.0"
+ android:valueTo="0.9"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in" />
+ <objectAnimator
+ android:duration="16"
+ android:propertyName="scaleX"
+ android:valueFrom="0.9"
+ android:valueTo="0.5"
+ android:valueType="floatType"
+ android:interpolator="@interpolator/btn_radio_to_off_mtrl_animation_interpolator_0" />
+ <objectAnimator
+ android:duration="300"
+ android:propertyName="scaleX"
+ android:valueFrom="0.5"
+ android:valueTo="1.0"
+ android:valueType="floatType"
+ android:interpolator="@interpolator/btn_radio_to_off_mtrl_animation_interpolator_0" />
+ </set>
+ <set
+ android:ordering="sequentially" >
+ <objectAnimator
+ android:duration="183"
+ android:propertyName="scaleY"
+ android:valueFrom="1.0"
+ android:valueTo="0.9"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in" />
+ <objectAnimator
+ android:duration="16"
+ android:propertyName="scaleY"
+ android:valueFrom="0.9"
+ android:valueTo="0.5"
+ android:valueType="floatType"
+ android:interpolator="@interpolator/btn_radio_to_off_mtrl_animation_interpolator_0" />
+ <objectAnimator
+ android:duration="300"
+ android:propertyName="scaleY"
+ android:valueFrom="0.5"
+ android:valueTo="1.0"
+ android:valueType="floatType"
+ android:interpolator="@interpolator/btn_radio_to_off_mtrl_animation_interpolator_0" />
+ </set>
+ </set>
+ </aapt:attr>
+ </target>
+
+ <target android:name="ring_outer_path">
+ <aapt:attr name="android:animation">
+ <set
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <set
+ android:ordering="sequentially">
+ <objectAnimator
+ android:duration="183"
+ android:propertyName="strokeWidth"
+ android:valueFrom="2.0"
+ android:valueTo="2.0"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ <objectAnimator
+ android:duration="16"
+ android:propertyName="strokeWidth"
+ android:valueFrom="2.0"
+ android:valueTo="18.0"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ <objectAnimator
+ android:duration="300"
+ android:propertyName="strokeWidth"
+ android:valueFrom="18.0"
+ android:valueTo="2.0"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ </set>
+
+ </set>
+ </aapt:attr>
+ </target>
+ <target
+ android:name="dot_group">
+ <aapt:attr name="android:animation">
+ <set
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <set
+ android:ordering="sequentially">
+ <objectAnimator
+ android:duration="183"
+ android:propertyName="scaleX"
+ android:valueFrom="1.0"
+ android:valueTo="1.4"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ <objectAnimator
+ android:duration="16"
+ android:propertyName="scaleX"
+ android:valueFrom="1.4"
+ android:valueTo="0.0"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ <objectAnimator
+ android:duration="300"
+ android:propertyName="scaleX"
+ android:valueFrom="0.0"
+ android:valueTo="0.0"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ </set>
+ <set
+ android:ordering="sequentially">
+ <objectAnimator
+ android:duration="183"
+ android:propertyName="scaleY"
+ android:valueFrom="1.0"
+ android:valueTo="1.4"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ <objectAnimator
+ android:duration="16"
+ android:propertyName="scaleY"
+ android:valueFrom="1.4"
+ android:valueTo="0.0"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ <objectAnimator
+ android:duration="300"
+ android:propertyName="scaleY"
+ android:valueFrom="0.0"
+ android:valueTo="0.0"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ </set>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/tests/VectorDrawableTest/res/interpolator/btn_radio_to_off_mtrl_animation_interpolator_0.xml b/tests/VectorDrawableTest/res/interpolator/btn_radio_to_off_mtrl_animation_interpolator_0.xml
new file mode 100644
index 0000000..d3728c4
--- /dev/null
+++ b/tests/VectorDrawableTest/res/interpolator/btn_radio_to_off_mtrl_animation_interpolator_0.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<pathInterpolator
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:pathData="M 0.0,0.0 c 0.4,0.0 0.4,1.0 1.0,1.0" />
diff --git a/tests/VectorDrawableTest/src/com/android/test/dynamic/AnimatedVectorDrawableTest.java b/tests/VectorDrawableTest/src/com/android/test/dynamic/AnimatedVectorDrawableTest.java
index 9351f63..4026e5e 100644
--- a/tests/VectorDrawableTest/src/com/android/test/dynamic/AnimatedVectorDrawableTest.java
+++ b/tests/VectorDrawableTest/src/com/android/test/dynamic/AnimatedVectorDrawableTest.java
@@ -29,6 +29,7 @@
private static final String LOGCAT = "AnimatedVectorDrawableTest";
protected int[] icon = {
+ R.drawable.btn_radio_on_to_off_bundle,
R.drawable.ic_rotate_2_portrait_v2_animation,
R.drawable.ic_signal_airplane_v2_animation,
R.drawable.ic_hourglass_animation,
diff --git a/tools/aapt2/integration-tests/StaticLibOne/Android.mk b/tools/aapt2/integration-tests/StaticLibOne/Android.mk
index d59dc60..0b7129a 100644
--- a/tools/aapt2/integration-tests/StaticLibOne/Android.mk
+++ b/tools/aapt2/integration-tests/StaticLibOne/Android.mk
@@ -22,4 +22,7 @@
LOCAL_MODULE_TAGS := tests
LOCAL_SRC_FILES := $(call all-java-files-under,src)
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+# We need this to compile the Java sources of AaptTestStaticLibTwo using javac.
+LOCAL_JAR_EXCLUDE_FILES := none
include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/wifi/java/android/net/wifi/WifiScanner.java b/wifi/java/android/net/wifi/WifiScanner.java
index 508bdff..73ddbbc 100644
--- a/wifi/java/android/net/wifi/WifiScanner.java
+++ b/wifi/java/android/net/wifi/WifiScanner.java
@@ -27,6 +27,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
+import android.os.WorkSource;
import android.util.Log;
import android.util.SparseArray;
@@ -160,6 +161,11 @@
*/
public static final int REPORT_EVENT_NO_BATCH = (1 << 2);
+
+ /** {@hide} */
+ public static final String SCAN_PARAMS_SCAN_SETTINGS_KEY = "ScanSettings";
+ /** {@hide} */
+ public static final String SCAN_PARAMS_WORK_SOURCE_KEY = "WorkSource";
/**
* scan configuration parameters to be sent to {@link #startBackgroundScan}
*/
@@ -661,12 +667,29 @@
* scans should also not share this object.
*/
public void startBackgroundScan(ScanSettings settings, ScanListener listener) {
+ startBackgroundScan(settings, listener, null);
+ }
+
+ /** start wifi scan in background
+ * @param settings specifies various parameters for the scan; for more information look at
+ * {@link ScanSettings}
+ * @param workSource WorkSource to blame for power usage
+ * @param listener specifies the object to report events to. This object is also treated as a
+ * key for this scan, and must also be specified to cancel the scan. Multiple
+ * scans should also not share this object.
+ */
+ public void startBackgroundScan(ScanSettings settings, ScanListener listener,
+ WorkSource workSource) {
Preconditions.checkNotNull(listener, "listener cannot be null");
int key = addListener(listener);
if (key == INVALID_KEY) return;
validateChannel();
- sAsyncChannel.sendMessage(CMD_START_BACKGROUND_SCAN, 0, key, settings);
+ Bundle scanParams = new Bundle();
+ scanParams.putParcelable(SCAN_PARAMS_SCAN_SETTINGS_KEY, settings);
+ scanParams.putParcelable(SCAN_PARAMS_WORK_SOURCE_KEY, workSource);
+ sAsyncChannel.sendMessage(CMD_START_BACKGROUND_SCAN, 0, key, scanParams);
}
+
/**
* stop an ongoing wifi scan
* @param listener specifies which scan to cancel; must be same object as passed in {@link
@@ -698,11 +721,27 @@
* scans should also not share this object.
*/
public void startScan(ScanSettings settings, ScanListener listener) {
+ startScan(settings, listener, null);
+ }
+
+ /**
+ * starts a single scan and reports results asynchronously
+ * @param settings specifies various parameters for the scan; for more information look at
+ * {@link ScanSettings}
+ * @param workSource WorkSource to blame for power usage
+ * @param listener specifies the object to report events to. This object is also treated as a
+ * key for this scan, and must also be specified to cancel the scan. Multiple
+ * scans should also not share this object.
+ */
+ public void startScan(ScanSettings settings, ScanListener listener, WorkSource workSource) {
Preconditions.checkNotNull(listener, "listener cannot be null");
int key = addListener(listener);
if (key == INVALID_KEY) return;
validateChannel();
- sAsyncChannel.sendMessage(CMD_START_SINGLE_SCAN, 0, key, settings);
+ Bundle scanParams = new Bundle();
+ scanParams.putParcelable(SCAN_PARAMS_SCAN_SETTINGS_KEY, settings);
+ scanParams.putParcelable(SCAN_PARAMS_WORK_SOURCE_KEY, workSource);
+ sAsyncChannel.sendMessage(CMD_START_SINGLE_SCAN, 0, key, scanParams);
}
/**
@@ -721,7 +760,6 @@
private void startPnoScan(ScanSettings scanSettings, PnoSettings pnoSettings, int key) {
// Bundle up both the settings and send it across.
Bundle pnoParams = new Bundle();
- if (pnoParams == null) return;
// Set the PNO scan flag.
scanSettings.isPnoScan = true;
pnoParams.putParcelable(PNO_PARAMS_SCAN_SETTINGS_KEY, scanSettings);